在WPF中公开内部控件属性以进行绑定

44

[编辑]: 我自己找到了解决方法。我发布了我的解决方案,希望能为其他人节省几天的谷歌搜索时间。如果你是WPF大师,请看看我的解决方案,并让我知道是否有更好/更优雅/更有效的方法做到这一点。特别是,我想知道我不知道的东西...这个解决方案会在将来给我带来什么问题吗?问题实际上归结为公开内部控件属性。

问题: 我正在创建一些代码,用于自动生成一个基于XML文件的数据绑定GUI。我有一个xsd文件可以帮助我确定节点类型等。简单的键/值元素很容易处理。

当我分析此元素时:

<Key>value</Key>

我可以创建一个新的"KeyValueControl"并将DataContext设置为该元素。这个KeyValueControl被定义为一个UserControl,并且只有一些简单的绑定操作。它适用于任何简单的XElement

这个控件内部的XAML代码如下:

<Label Content={Binding Path=Name} /> 
<TextBox Text={Binding Path=Value} />
结果是一个带有元素名称标签和值文本框的行,我可以编辑它的值。
现在,有时候我需要显示查找值而不是实际值。我想创建一个类似于上面的KeyValueControl但可以指定(基于文件中的信息)ItemsSource、DisplayMemberPath和ValueMemberPath的“KeyValueComboBox”。DisplayMemberPath和ValueMemberPath绑定与KeyValueControl相同。
我不知道标准用户控件是否能够处理此问题,还是我需要继承自Selector。
该控件中的XAML如下:
<Label Content={Binding Path=Name} /> 
<ComboBox SelectedValue={Binding Path=Value}
          ItemsSource={Binding [BOUND TO THE ItemsSource PROPERTY OF THIS CUSTOM CONTROL]
          DisplayMemberPath={Binding [BOUND TO THE DisplayMemberPath OF THIS CUSTOM CONTROL]
          SelectedValuePath={Binding [BOUND TO THE SelectedValuePath OF THIS CUSTOM CONTROL]/>

在我的代码中,我会像这样做(假设该节点是一个“Thing”,需要显示一组“Thing”,以便用户可以选择ID):

var myBoundComboBox = new KeyValueComboBox();
myBoundComboBox.ItemsSource = getThingsList();
myBoundComboBox.DisplayMemberPath = "ThingName";
myBoundComboBox.ValueMemberPath = "ThingID"
myBoundComboBox.DataContext = thisXElement;
...
myStackPanel.Children.Add(myBoundComboBox)

那么我的问题是:

1)我应该从 Control 还是 Selector 继承我的 KeyValueComboBox

2)如果我应该从 Control 继承,如何公开内部组合框的 ItemsSourceDisplayMemberPathValueMemberPath 以供绑定?

3)如果我需要从 Selector 继承,可以有人提供一个小例子,说明我该如何入门?因为我对 WPF 还不熟悉,所以如果这是我需要采取的方法,一个漂亮简单的例子会真正有所帮助。

2个回答

52

我最终自己想出了如何实现。在这里发布答案,以便其他人可以看到一个有效的解决方案,或者WPF大师能够提供更好/更优雅的方法。

因此,答案最终是第2种方法。 公开内部属性结果是正确的答案。 实际上,一旦你知道该怎么做,设置它其实很容易。 (我找不到很多完整的例子),因此希望这个例子能帮助遇到这个问题的其他人。

ComboBoxWithLabel.xaml.cs

这个文件中重要的是使用DependencyProperties。请注意,我们现在所做的只是公开属性(LabelContentItemsSource)。 XAML将负责将内部控件的属性连接到这些外部属性。

namespace BoundComboBoxExample
{
    /// <summary>
    /// Interaction logic for ComboBoxWithLabel.xaml
    /// </summary>
    public partial class ComboBoxWithLabel : UserControl
    {
        // Declare ItemsSource and Register as an Owner of ComboBox.ItemsSource
        // the ComboBoxWithLabel.xaml will bind the ComboBox.ItemsSource to this
        // property
        public IEnumerable ItemsSource
        {
            get { return (IEnumerable)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }

        public static readonly DependencyProperty ItemsSourceProperty =
          ComboBox.ItemsSourceProperty.AddOwner(typeof(ComboBoxWithLabel));

        // Declare a new LabelContent property that can be bound as well
        // The ComboBoxWithLable.xaml will bind the Label's content to this
        public string LabelContent
        {
            get { return (string)GetValue(LabelContentProperty); }
            set { SetValue(LabelContentProperty, value); }
        }

        public static readonly DependencyProperty LabelContentProperty =
          DependencyProperty.Register("LabelContent", typeof(string), typeof(ComboBoxWithLabel));
      
        public ComboBoxWithLabel()
        {
            InitializeComponent();
        }
    }
}

ComboBoxWithLabel.xaml

XAML很简单,除了Label和ComboBox ItemsSource上的绑定之外。我发现将属性在.cs文件中声明(如上所示),然后使用VS2010设计器从属性窗格设置绑定源是最容易正确地完成这些绑定的方法。实际上,这是我知道的将内部控件属性绑定到基础控件的唯一方法。如果有更好的方法,请告诉我。

<UserControl x:Class="BoundComboBoxExample.ComboBoxWithLabel"
             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" 
             mc:Ignorable="d" 
             d:DesignHeight="28" d:DesignWidth="453" xmlns:my="clr-namespace:BoundComboBoxExample">
    <Grid>
        <DockPanel LastChildFill="True">
            <!-- This will bind the Content property on the label to the 'LabelContent' 
                 property on this control-->
            <Label Content="{Binding Path=LabelContent, 
                             RelativeSource={RelativeSource FindAncestor, 
                                             AncestorType=my:ComboBoxWithLabel, 
                                             AncestorLevel=1}}" 
                   Width="100" 
                   HorizontalAlignment="Left"/>
            <!-- This will bind the ItemsSource of the ComboBox to this 
                 control's ItemsSource property -->
            <ComboBox ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, 
                                    AncestorType=my:ComboBoxWithLabel, 
                                    AncestorLevel=1}, 
                                    Path=ItemsSource}"></ComboBox>
            <!-- you can do the same thing with SelectedValuePath, 
                 DisplayMemberPath, etc, but this illustrates the technique -->
        </DockPanel>
            
    </Grid>
</UserControl>

MainWindow.xaml

使用这个的 XAML 并不令人感兴趣,这正是我想要的。 您可以通过所有标准的 WPF 技术设置 ItemsSourceLabelContent

<Window x:Class="BoundComboBoxExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="86" Width="464" xmlns:my="clr-namespace:BoundComboBoxExample"
        Loaded="Window_Loaded">
    <Window.Resources>
        <ObjectDataProvider x:Key="LookupValues" />
    </Window.Resources>
    <Grid>
        <my:ComboBoxWithLabel LabelContent="Foo"
                              ItemsSource="{Binding Source={StaticResource LookupValues}}"
                              HorizontalAlignment="Left" 
                              Margin="12,12,0,0" 
                              x:Name="comboBoxWithLabel1" 
                              VerticalAlignment="Top" 
                              Height="23" 
                              Width="418" />
    </Grid>
</Window>

为了完整起见,这是MainWindow.xaml.cs的内容。

/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        ((ObjectDataProvider)FindResource("LookupValues")).ObjectInstance =
            (from i in Enumerable.Range(0, 5)
             select string.Format("Bar {0}", i)).ToArray();

    }
}

我正在使用类似的技术,但是当我的项本身设置为使用RelativeSource绑定时遇到了麻烦。因此,在您的情况下,如果您将ComboBoxItem元素的集合分配为ItemsSource,并且其中一个元素使用RelativeSource绑定到逻辑树上的某个东西,通过使用FindAncestor进行绑定以绑定到在该祖先元素上定义的属性,则绑定将失败。 - jpierson
1
我之前在类似的情况下使用过这个解决方法,回答得很好。在我的应用程序中,我正在绑定到 ComboBox 的 SelectedItem。需要注意的是,为了使绑定正常工作,在某些情况下需要添加绑定模式 = twoway 和 UpdateSourceTrigger = PropertyChanged。 - Cocomico

1
我尝试了您的解决方案,但对我无效。它根本没有将值传递到内部控件。我的做法是在外部控件中声明相同的依赖属性,并将内部控件绑定到外部控件:
    // Declare IsReadOnly property and Register as an Owner of TimePicker (base InputBase).IsReadOnly the TimePickerEx.xaml will bind the TimePicker.IsReadOnly to this property
    // does not work: public static readonly DependencyProperty IsReadOnlyProperty = InputBase.IsReadOnlyProperty.AddOwner(typeof(TimePickerEx));

    public static readonly DependencyProperty IsReadOnlyProperty = DependencyProperty.Register("IsReadOnly", typeof (bool), typeof (TimePickerEx), new PropertyMetadata(default(bool)));
    public bool IsReadOnly
    {
        get { return (bool) GetValue(IsReadOnlyProperty); }
        set { SetValue(IsReadOnlyProperty, value); }
    }

比在XAML中:
  <UserControl x:Class="CBRControls.TimePickerEx" x:Name="TimePickerExControl"
        ...
        >

      <xctk:TimePicker x:Name="Picker" 
              IsReadOnly="{Binding ElementName=TimePickerExControl, Path=IsReadOnly}"
              ...
       />

  </UserControl>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接