简体   繁体   English

绑定到UserControl中ComboBox的SelectedItem

[英]Binding to SelectedItem of ComboBox in UserControl

I have a UserControl consisting of a ComboBox with a Label. 我有一个由带有标签的ComboBox组成的UserControl。 I am looking to update a screen with an instance of this ComboBox and dynamically create UserControls in a StackPanel, based on the SelectedItem value. 我希望使用此ComboBox的实例更新屏幕,并根据SelectedItem值在StackPanel中动态创建UserControls。

I currently have a screen with an instance of this ComboBox and have it binding the following way: 我目前在屏幕上有此ComboBox的实例,并通过以下方式绑定它:

Pseudocode Example (removing unrelated code): 伪代码示例(删除不相关的代码):

<!-- 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}"/>

I am new to WPF and databinding, so not sure the best way to implement this. 我是WPF和数据绑定的新手,所以不确定实现此功能的最佳方法。 Basically, on the screen: when MyComboBoxExampleInstance selection changes, dynamically set the controls of a StackPanel on the screen. 基本上,在屏幕上:当MyComboBoxExampleInstance选择更改时,在屏幕上动态设置StackPanel的控件。 I am not sure how to properly hook in to the SelectionChanged event of a child object of a UserControl. 我不确定如何正确连接到UserControl的子对象的SelectionChanged事件。

Any thoughts, corrections, and (constructive) criticism is appreciated. 任何想法,更正和(建设性)批评都值得赞赏。 Thanks for any help in advance. 感谢您的任何帮助。

There are several ways to go about this. 有几种方法可以解决此问题。 Here's one way. 这是一种方法。 It's not necessarily the best way but it's easy to understand. 这不一定是最好的方法,但很容易理解。

First, the user control xaml. 首先,用户控制xaml。 Note the binding of the ItemsSource property on the user control, which specifies MyComboBoxItems as the items source. 请注意用户控件上ItemsSource属性的绑定,该控件将MyComboBoxItems指定为项目源。 More on where that comes from in a bit. 进一步了解它的来源。

 <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>

Now the code-behind, MyUserControl.xaml.cs. 现在,隐藏代码MyUserControl.xaml.cs。 We provide an combobox selection changed event handler that in turn raises a custom event, MyComboBoxSelectionChanged, which is defined by the event argument class and delegate handler at the bottom of the code. 我们提供了一个组合框选择更改事件处理程序,该处理程序反过来引发了一个自定义事件MyComboBoxSelectionChanged,该事件由代码底部的事件参数类和委托处理程序定义。 Our OnSelectionChanged method simply forwards the selection change event via the custom event we've defined. 我们的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);

}

Now we go to our MainWindow.xaml, where we define an instance of MyUserControl and set a handler for the custom event we defined. 现在,我们转到MainWindow.xaml,在其中定义MyUserControl的实例,并为定义的自定义事件设置处理程序。 We also provide a StackPanel to host the items that will be created on a selection changed event. 我们还提供了一个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>

Now the code-behind for MainWindow.xaml. 现在是MainWindow.xaml的代码隐藏。 Here we define a public property containing a list of objects of type MyComboBoxItem (defined at the bottom of the file), and we initialize the array with some values. 在这里,我们定义一个公共属性,其中包含MyComboBoxItem类型的对象的列表(在文件底部定义),并使用一些值初始化该数组。

Recall that we set the ItemsSource property of the ComboBox inside MyUserControl to "{Binding MyComboBoxItems}", so the question is, how does the property defined in the MainWindow magically become available in MyUserControl? 回想一下,我们将MyUserControl内ComboBox的ItemsSource属性设置为“ {Binding MyComboBoxItems}”,所以问题是,如何在MainWindow中定义的属性在MyUserControl中神奇地可用?

In WPF, DataContext values are inherited from parent controls if they aren't explicitly set, and since we did not specify a data context for the control, the instance of MyUserControl inherits the DataContext of the parent window. 在WPF中,如果未显式设置DataContext值,则它们将从父控件继承,并且由于我们没有为控件指定数据上下文,因此MyUserControl的实例将继承父窗口的DataContext。 In the constructor we set the MainWindow data context to refer to itself, so the MyComboBoxItems list is available to any child controls (and their children, and so on.) 在构造函数中,我们将MainWindow数据上下文设置为引用自身,因此MyComboBoxItems列表可用于任何子控件(及其子控件,等等)。

Typically we'd go ahead and add a dependency property for the user control called ItemsSource and in the user control we'd bind the ComboBox's ItemsSource property to the dependency property rather than to MyComboxItems. 通常,我们会继续为用户控件添加一个名为ItemsSource的依赖项属性,然后在用户控件中将ComboBox的ItemsSource属性绑定到该依赖项属性而不是MyComboxItems。 MainWindow.xaml would then bind it's collection directly to the dependency property on the user control. 然后MainWindow.xaml将其集合直接绑定到用户控件上的依赖项属性。 This helps make the user control more re-usable since it wouldn't depend on specific properties defined in an inherited data context. 这有助于使用户控件更加可重用,因为它不依赖于在继承的数据上下文中定义的特定属性。

Finally, in the event handler for the user control's custom event we obtain the value selected by the user and create a UserControl populated with a text box (all with various properties set to make the items interesting visually) and we directly add them to the Children property of the StackPanel. 最后,在用户控件的自定义事件的事件处理程序中,我们获取用户选择的值,并创建一个由文本框填充的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; }
    }
}

Finally, I'd consider using an ItemsControl or a ListBox bound to an ObservableCollection rather than sticking things into a StackPanel. 最后,我考虑使用绑定到ObservableCollection的ItemsControl或ListBox,而不是将其粘贴到StackPanel中。 You could define a nice data template for the user control to display and maybe a DataTemplateSelector to use different user controls based on settings in the data item. 您可以根据数据项中的设置定义一个不错的数据模板供用户控件显示,也可以定义一个DataTemplateSelector来使用不同的用户控件。 This would allow me to simply add the reference to the MyComboBoxItem obtained in the selection changed handler to that collection, and the binding machinery would automatically generate a new item using the data template I defined and create the necessary visual elements to display it. 这将使我可以简单地将对在选择更改处理程序中获得的MyComboBoxItem的引用添加到该集合,并且绑定机制将使用我定义的数据模板自动生成一个新项目,并创建必要的可视元素来显示它。

So given all that, here are the changes to do all that. 因此,鉴于所有这些,这里是进行所有更改的更改。

First, we modify our data item to add a color property. 首先,我们修改数据项以添加color属性。 We'll use that property to determine how we display the selected item: 我们将使用该属性来确定我们如何显示所选项目:

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

Now we implement INotifyPropertyChanged in MainWindow.xaml.cs to let the WPF binding engine update the UI when we change properties. 现在,我们在MainWindow.xaml.cs中实现INotifyPropertyChanged,以使WPF绑定引擎在更改属性时更新UI。 This is the event handler and a helper method, OnPropertyChanged. 这是事件处理程序和帮助程序方法OnPropertyChanged。

We also modify the combo box initializer to add a value for the Color property. 我们还修改了组合框初始化程序,以为Color属性添加一个值。 We'll leave on blank for fun. 我们将留空玩耍。

We then add a new ObservableCollect, "ActiveUserControls" to store the MyComboBoxItem received in the combo box selection changed event. 然后,我们添加一个新的ObservableCollect“ ActiveUserControls”,以存储在组合框选择更改事件中收到的MyComboBoxItem。 We do that instead of creating user controls on the fly in code. 我们这样做不是在代码中动态创建用户控件。

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);
        }
    }
}

Now let's look at some changes we made to MyUserControl. 现在,让我们看看我们对MyUserControl所做的一些更改。 We've modified the combo box ItemsSource to point at a property, ItemsSource defined in MyUserControl, and we also map the ItemTemplate to an ItemTemplate property in 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>

Here's were we define those new properties in MyUserControl.cs. 我们在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]});
        }
    }
}

Let's look at how we bind to those in MainWindow.xaml: 让我们看一下如何绑定到MainWindow.xaml中的内容:

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

So now we can bind our items directly and provide our own data template to specify how the combobox should display the item. 因此,现在我们可以直接绑定项目,并提供自己的数据模板来指定组合框应如何显示项目。

Finally, I want to replace the StackPanel with an ItemsControl. 最后,我想用ItemsControl替换StackPanel。 This is like a ListBox without scrolling or item selection support. 这就像一个没有滚动或项目选择支持的列表框。 In fact, ListBox is derived from ItemsControl. 实际上,ListBox派生自ItemsControl。 I also want to use a different user control in the list based on the value of the Color property. 我还想基于Color属性的值在列表中使用其他用户控件。 To do that, we define some data templates for each value in MainWindow.Xaml: 为此,我们为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>

Here's RedUserControl. 这是RedUserControl。 Green is the same with a different foreground color. 绿色与前景色不同。

<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>

Now the trick is to use the right data template based on the color value. 现在的诀窍是根据颜色值使用正确的数据模板。 For that we create a DataTemplateSelector. 为此,我们创建一个DataTemplateSelector。 This is called by WPF for each item to be displayed. WPF为要显示的每个项目调用此方法。 We can examine the data context object a choose which data template to use: 我们可以检查数据上下文对象,然后选择要使用的数据模板:

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;
    }
}

We create an instance of our data template selector in xaml in MainWindow.xaml: 我们在MainWindow.xaml中的xaml中创建数据模板选择器的实例:

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

Finally we replace our stack panel with an Items control: 最后,我们将堆栈面板替换为Items控件:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM