首页 > 解决方案 > 绑定到 UserControl 中 ComboBox 的 SelectedItem

问题描述

我有一个由带有标签的组合框组成的用户控件。我希望使用此 ComboBox 的实例更新屏幕,并根据 SelectedItem 值在 StackPanel 中动态创建 UserControls。

我目前有一个带有此 ComboBox 实例的屏幕,并通过以下方式绑定:

伪代码示例(删除无关代码):

<!-- MyComboBoxExample.xaml -->    
<ComboBox x:Name="myComboBox" SelectedValuePath="Key" DisplayMemberPath="Value" ItemsSource="{Binding MyBoxItems}/>
/* MyComboBoxExample.xaml.cs */

public static readonly DependencyProperty MyBoxItemsProperty = DependencyProperty.Register("MyBoxItems", typeof(Dictionary<string, string>),
    typeof(MyComboBoxExample), new PropertyMetadata(null));
<!-- MyScreen.xaml -->    
<local:MyComboBoxExample x:Name="MyComboBoxExampleInstance" MyBoxItems="{Binding Descriptions}"/>

我是 WPF 和数据绑定的新手,所以不确定实现这一点的最佳方法。基本上,在屏幕上:当 MyComboBoxExampleInstance 选择发生变化时,动态设置一个 StackPanel 在屏幕上的控件。我不确定如何正确连接到 UserControl 的子对象的 SelectionChanged 事件。

任何想法、更正和(建设性的)批评都值得赞赏。感谢您提前提供任何帮助。

标签: c#wpfxamldata-bindingwpf-controls

解决方案


有几种方法可以解决这个问题。这是一种方法。这不一定是最好的方法,但很容易理解。

一、用户控件xaml。请注意用户控件上 ItemsSource 属性的绑定,它将 MyComboBoxItems 指定为项目源。更多关于它的来源。

 <UserControl x:Class="WpfApp1.MyUserControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WpfApp1"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
        <Grid>
            <ComboBox Height="Auto" ItemsSource="{Binding MyComboBoxItems}" SelectionChanged="OnSelectionChanged">
               <ComboBox.ItemTemplate>
                   <DataTemplate>
                       <TextBlock Text="{Binding Text}"/>
                   </DataTemplate>
               </ComboBox.ItemTemplate>
           </ComboBox>
        </Grid>
    </UserControl>

现在是代码隐藏 MyUserControl.xaml.cs。我们提供了一个组合框选择更改事件处理程序,该处理程序反过来引发一个自定义事件 MyComboBoxSelectionChanged,该事件由代码底部的事件参数类和委托处理程序定义。我们的 OnSelectionChanged 方法只是通过我们定义的自定义事件转发选择更改事件。

using System;
using System.Windows.Controls;

namespace WpfApp1
{
    /// <summary>
    /// Interaction logic for MyUserControl.xaml
    /// </summary>
    public partial class MyUserControl : UserControl
    {
        public event MyComboBoxSelectionChangedEventHandler MyComboBoxSelectionChanged;
        public MyUserControl()
        {
            InitializeComponent();
        }

        private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {

            if (e.AddedItems.Count > 0)
            {
                MyComboBoxSelectionChanged?.Invoke(this,
                    new MyComboBoxSelectionChangedEventArgs() {MyComboBoxItem = e.AddedItems[0]});
            }
        }
    }

    public class MyComboBoxSelectionChangedEventArgs : EventArgs
    {
        public object MyComboBoxItem { get; set; }
    }

    public delegate void MyComboBoxSelectionChangedEventHandler(object sender, MyComboBoxSelectionChangedEventArgs e);

}

现在我们转到 MainWindow.xaml,我们在其中定义 MyUserControl 的一个实例并为我们定义的自定义事件设置一个处理程序。我们还提供了一个 StackPanel 来托管将在选择更改事件上创建的项目。

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow"
        Height="450"
        Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>

        <local:MyUserControl Width="140" Height="32" DataContext="{Binding}"  Grid.Row="0" MyComboBoxSelectionChanged="OnSelectionChanged"></local:MyUserControl>

        <StackPanel Grid.Row="1" x:Name="MyUserControls"/>
    </Grid>

</Window>

现在是 MainWindow.xaml 的代码隐藏。在这里,我们定义了一个包含 MyComboBoxItem 类型的对象列表的公共属性(在文件底部定义),我们用一些值初始化数组。

回想一下,我们将 MyUserControl 中 ComboBox 的 ItemsSource 属性设置为“{Binding MyComboBoxItems}”,那么问题是,MainWindow 中定义的属性如何神奇地在 MyUserControl 中可用?

在 WPF 中,如果未显式设置 DataContext 值,则从父控件继承,并且由于我们没有为控件指定数据上下文,因此 MyUserControl 的实例继承了父窗口的 DataContext。在构造函数中,我们将 MainWindow 数据上下文设置为引用自身,因此 MyComboBoxItems 列表可用于任何子控件(及其子控件,等等)。

通常,我们会继续为名为 ItemsSource 的用户控件添加依赖属性,并且在用户控件中,我们会将 ComboBox 的 ItemsSource 属性绑定到依赖属性而不是 MyComboxItems。然后 MainWindow.xaml 会将其集合直接绑定到用户控件上的依赖项属性。这有助于使用户控件更具可重用性,因为它不依赖于在继承的数据上下文中定义的特定属性。

最后,在用户控件的自定义事件的事件处理程序中,我们获取用户选择的值并创建一个填充有文本框的 UserControl(所有这些都设置了各种属性以使项目在视觉上变得有趣),我们直接将它们添加到 Children StackPanel 的属性。

using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace WpfApp1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public List<MyComboBoxItem> MyComboBoxItems { get; set; } = new List<MyComboBoxItem>()
        {
            new MyComboBoxItem() {Text = "Item1"},
            new MyComboBoxItem() {Text = "Item2"},
            new MyComboBoxItem() {Text = "Item3"},

        };
        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;
        }

        private void OnSelectionChanged(object sender, MyComboBoxSelectionChangedEventArgs e)
        {
            if (e.MyComboBoxItem is MyComboBoxItem item)
            {
                MyUserControls.Children.Add(
                new UserControl()
                {
                    Margin = new Thickness(2),
                    Background = new SolidColorBrush(Colors.LightGray),
                    Content = new TextBlock()
                    {
                        Margin = new Thickness(4),
                        VerticalAlignment = VerticalAlignment.Center,
                        HorizontalAlignment = HorizontalAlignment.Center,
                        FontSize = 48,
                        FontWeight = FontWeights.Bold,
                        Foreground = new SolidColorBrush(Colors.DarkGreen),
                        Text = item.Text
                    }
                });
            }
        }
    }

    public class MyComboBoxItem
    {
        public string Text { get; set; }
    }
}

最后,我会考虑使用绑定到 ObservableCollection 的 ItemsControl 或 ListBox,而不是将内容粘贴到 StackPanel 中。您可以为要显示的用户控件定义一个不错的数据模板,也可以定义一个 DataTemplateSelector 以根据数据项中的设置使用不同的用户控件。这将允许我简单地将在选择更改处理程序中获得的 MyComboBoxItem 的引用添加到该集合,绑定机制将使用我定义的数据模板自动生成一个新项目,并创建必要的可视元素来显示它。

因此,鉴于所有这些,这里是做所有这些的更改。

首先,我们修改数据项以添加颜色属性。我们将使用该属性来确定我们如何显示所选项目:

public class MyComboBoxItem
{
    public string Color { get; set; }
    public string Text { get; set; }
}

现在我们在 MainWindow.xaml.cs 中实现 INotifyPropertyChanged,让 WPF 绑定引擎在我们更改属性时更新 UI。这是事件处理程序和辅助方法 OnPropertyChanged。

我们还修改了组合框初始值设定项,为 Color 属性添加了一个值。为了好玩,我们将留空。

然后我们添加一个新的 ObservableCollect,“ActiveUserControls”来存储在组合框选择更改事件中收到的 MyComboBoxItem。我们这样做,而不是在代码中动态创建用户控件。

public partial class MainWindow : Window, INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    public List<MyComboBoxItem> MyComboBoxItems { get; set; } = new List<MyComboBoxItem>()
    {
        new MyComboBoxItem() {Text = "Item1", Color = "Red"},
        new MyComboBoxItem() {Text = "Item2", Color = "Green"},
        new MyComboBoxItem() {Text = "Item3"},
    };

    private ObservableCollection<MyComboBoxItem> _activeUserControls;
    public ObservableCollection<MyComboBoxItem> ActiveUserControls
    {
        get => _activeUserControls;
        set { _activeUserControls = value; OnPropertyChanged(); }
    }

    public MainWindow()
    {
        InitializeComponent();
        DataContext = this;
    }

    private void OnSelectionChanged(object sender, MyComboBoxSelectionChangedEventArgs e)
    {
        if (e.MyComboBoxItem is MyComboBoxItem item)
        {
            if (ActiveUserControls == null)
            {
                ActiveUserControls = new ObservableCollection<MyComboBoxItem>();
            }

            ActiveUserControls.Add(item);
        }
    }
}

现在让我们看看我们对 MyUserControl 所做的一些更改。我们修改了组合框 ItemsSource 以指向 MyUserControl 中定义的 ItemsSource 属性,我们还将 ItemTemplate 映射到 MyUserControl 中的 ItemTemplate 属性。

<UserControl x:Class="WpfApp1.MyUserControl"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         xmlns:local="clr-namespace:WpfApp1"
         mc:Ignorable="d"
         d:DesignHeight="450"
         d:DesignWidth="800">
    <Grid>
        <ComboBox Height="Auto"
              ItemsSource="{Binding ItemsSource, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MyUserControl}}}"
              ItemTemplate="{Binding ItemTemplate, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MyUserControl}}}"
              SelectionChanged="OnSelectionChanged">

        </ComboBox>
    </Grid>
</UserControl>

这是我们在 MyUserControl.cs 中定义的那些新属性。

public partial class MyUserControl : UserControl
{
    public event MyComboBoxSelectionChangedEventHandler MyComboBoxSelectionChanged;
    public MyUserControl()
    {
        InitializeComponent();
    }

    public static readonly DependencyProperty ItemsSourceProperty =
        DependencyProperty.Register("ItemsSource",
            typeof(System.Collections.IEnumerable),
            typeof(MyUserControl),
            new PropertyMetadata(null));

    public System.Collections.IEnumerable ItemsSource
    {
        get => GetValue(ItemsSourceProperty) as IEnumerable;
        set => SetValue(ItemsSourceProperty, (IEnumerable)value);
    }

    public static readonly DependencyProperty ItemTemplateProperty =
        DependencyProperty.Register("ItemTemplate",
            typeof(DataTemplate),
            typeof(MyUserControl),
            new PropertyMetadata(null));

    public DataTemplate ItemTemplate
    {
        get => GetValue(ItemTemplateProperty) as DataTemplate;
        set => SetValue(ItemTemplateProperty, (DataTemplate)value);
    }

    private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
    {

        if (e.AddedItems.Count > 0)
        {
            MyComboBoxSelectionChanged?.Invoke(this,
                new MyComboBoxSelectionChangedEventArgs() {MyComboBoxItem = e.AddedItems[0]});
        }
    }
}

让我们看看我们如何绑定到 MainWindow.xaml 中的那些:

<local:MyUserControl Width="140"
                         Height="32"
                         Grid.Row="0"
                         MyComboBoxSelectionChanged="OnSelectionChanged"
                         ItemsSource="{Binding MyComboBoxItems}"
                         ItemTemplate="{StaticResource ComboBoxItemDataTemplate}" />

所以现在我们可以直接绑定我们的项目并提供我们自己的数据模板来指定组合框应该如何显示项目。

最后,我想用 ItemsControl 替换 StackPanel。这就像一个没有滚动或项目选择支持的 ListBox。实际上,ListBox 是从 ItemsControl 派生的。我还想根据 Color 属性的值在列表中使用不同的用户控件。为此,我们为 MainWindow.Xaml 中的每个值定义了一些数据模板:

    <DataTemplate x:Key="ComboBoxItemDataTemplate"
                  DataType="local:MyComboBoxItem">
        <StackPanel Orientation="Horizontal">
            <TextBlock Margin="4"
                       Text="{Binding Text}" />
            <TextBlock Margin="4"
                       Text="{Binding Color}" />
        </StackPanel>
    </DataTemplate>

    <DataTemplate x:Key="GreenUserControlDataTemplate"
                  DataType="local:MyComboBoxItem">
        <local:GreenUserControl DataContext="{Binding}" />
    </DataTemplate>

    <DataTemplate x:Key="RedUserControlDataTemplate"
                  DataType="local:MyComboBoxItem">
        <local:RedUserControl DataContext="{Binding}" />
    </DataTemplate>

    <DataTemplate x:Key="UnspecifiedUserControlDataTemplate"
                  DataType="local:MyComboBoxItem">
        <TextBlock Margin="4"
                   Text="{Binding Text}" />
    </DataTemplate>

这是 RedUserControl。绿色是相同的,具有不同的前景色。

<UserControl x:Class="WpfApp1.RedUserControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:WpfApp1"
             mc:Ignorable="d"
             d:DesignHeight="450"
             d:DesignWidth="800">
    <Grid Background="LightGray"
          Margin="2">
        <TextBlock Margin="4"
                   Foreground="DarkRed"
                   TextWrapping="Wrap"
                   Text="{Binding Text}"
                   FontSize="24"
                   FontWeight="Bold" />
    </Grid>
</UserControl>

现在的诀窍是根据颜色值使用正确的数据模板。为此,我们创建了一个 DataTemplateSelector。这由 WPF 为要显示的每个项目调用。我们可以检查数据上下文对象并选择要使用的数据模板:

public class UserControlDataTemplateSelector : DataTemplateSelector
{

    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        if (container is FrameworkElement fe)
        {
            if (item is MyComboBoxItem cbItem)
            {
                if (cbItem.Color == "Red")
                {
                    return fe.FindResource("RedUserControlDataTemplate") as DataTemplate;
                }
                if (cbItem.Color == "Green")
                {
                    return fe.FindResource("GreenUserControlDataTemplate") as DataTemplate;
                }
                return fe.FindResource("UnspecifiedUserControlDataTemplate") as DataTemplate;
            }
        }
        return null;
    }
}

我们在 MainWindow.xaml 中的 xaml 中创建数据模板选择器的实例:

<Window.Resources>
    <local:UserControlDataTemplateSelector x:Key="UserControlDataTemplateSelector" />
...

最后,我们用 Items 控件替换我们的堆栈面板:

   <ItemsControl Grid.Row="1"
                  x:Name="MyUserControls"
                  ItemsSource="{Binding ActiveUserControls}"
                  ItemTemplateSelector="{StaticResource UserControlDataTemplateSelector}" />

推荐阅读