在 WPF 中公开用于绑定的内部控件属性

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/4169090/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-08 22:18:42  来源:igfitidea点击:

Exposing inner Control properties for binding in WPF

wpfbindinguser-controlscombobox

提问by fbl

[Edit]: I figured out how to do this on my own. I posted my solution in the hope that it will save someone else a few days of googling. If you are a WPF guru, please look at my solution and let me know if there is a better / more elegant / more efficient way to do this. In particular, I am interested in knowing what I don't know... how is this solution going to screw me down the road? The problem really boils down to exposing inner control properties.

[编辑]:我自己想出了如何做到这一点。我发布了我的解决方案,希望它能为其他人节省几天的谷歌搜索时间。如果您是 WPF 大师,请查看我的解决方案,让我知道是否有更好/更优雅/更有效的方法来做到这一点。特别是,我有兴趣知道我不知道的事情......这个解决方案如何让我陷入困境?问题实际上归结为暴露内部控制属性。

Problem: I am creating some code to auto-generate a data-bound GUI in WPF for an XML file. I have an xsd file that can help me determine the node types, etc. Simple Key/Value elements are easy.

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

When I parse this element:

当我解析这个元素时:

<Key>value</Key>

I can create a new 'KeyValueControl' and set the DataContextto this element. The KeyValue is defined as a UserControl and just has some simple bindings on it. It works great for any simple XElement.

我可以创建一个新的“KeyValueControl”并将其设置DataContext为此元素。KeyValue 被定义为一个 UserControl 并且只有一些简单的绑定。它适用于任何简单的 XElement。

The XAML inside this control looks like this:

此控件中的 XAML 如下所示:

<Label Content={Binding Path=Name} /> 
<TextBox Text={Binding Path=Value} />

The result is a line that has a label with the element name and a text box with the value that I can edit.

结果是一行,其中包含一个带有元素名称的标签和一个带有我可以编辑的值的文本框。

Now, there are times where I need to display lookup values instead of the actual value. I would like to create a 'KeyValueComboBox' similar to the above KeyValueControl but be able to specify (based on information in the file) the ItemsSource, Display and Value paths. The 'Name' and 'Value' bindings would be the same as the KeyValueControl.

现在,有时我需要显示查找值而不是实际值。我想创建一个类似于上述 KeyValueControl 的“KeyValueComboBox”,但能够指定(基于文件中的信息)ItemsSource、Display 和 Value 路径。'Name' 和 'Value' 绑定将与 KeyValueControl 相同。

I don't know if a standard user control can handle this, or if I need to inherit from Selector.

我不知道标准用户控件是否可以处理这个问题,或者我是否需要从 Selector 继承。

The XAML in the control would look something like this:

控件中的 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]/>

In my code, I would then do something like this (assuming that this node is a 'Thing' and needs to display a list of Things so the user can select the ID:

在我的代码中,我会做这样的事情(假设这个节点是一个“事物”并且需要显示事物列表以便用户可以选择 ID:

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

So my questions are:

所以我的问题是:

1) Should I inherit my KeyValueComboBox from Control or Selector?

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

2) If I should inherit from Control, how do I expose the inner Combo Box's ItemsSource, DisplayMemberPath, and ValueMemberPath for binding?

2) 如果我应该从 Control 继承,我如何公开内部 Combo Box 的 ItemsSource、DisplayMemberPath 和 ValueMemberPath 以进行绑定?

3) If I need to inherit from Selector, can someone provide a small example of how I might get started with that? Again, I'm new to WPF so a nice, simple example would really help if that's the road I need to take.

3)如果我需要从 Selector 继承,有人可以提供一个小例子来说明我如何开始吗?同样,我是 WPF 的新手,所以如果这是我需要走的路,那么一个漂亮、简单的示例真的会有所帮助。

回答by fbl

I ended up figuring how how to do this on my own. I'm posting the answer here so that others can see a solution that works, and maybe a WPF guru will come by and show me a better/more elegant way to do this.

我最终想出了如何自己做到这一点。我在这里发布答案,以便其他人可以看到一个有效的解决方案,也许 WPF 大师会过来向我展示一个更好/更优雅的方法来做到这一点。

So, the answer ended up being #2. Exposing the inner properties turns out to be the right answer. Setting it up is actually pretty easy.. once you know how to do it. There aren't many complete examples of this (that I could find), so hopefully this one will help someone else that runs into this problem.

所以,答案最终是#2。事实证明,暴露内部属性是正确的答案。设置它实际上很容易......一旦你知道如何去做。没有很多完整的例子(我能找到),所以希望这个例子能帮助遇到这个问题的其他人。

ComboBoxWithLabel.xaml.cs

ComboBoxWithLabel.xaml.cs

The important thing in this file is the use of DependencyProperties. Note that all we're doing right now is just exposing the properties (LabelContent and ItemsSource). The XAML will take care of wiring the internal control's properties to these external properties.

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Collections;

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

ComboBoxWithLabel.xaml

The Xaml is pretty straightforward, with the exception of the bindings on the Label and the ComboBox ItemsSource. I found that the easiest way to get these bindings right is to declare the properties in the .cs file (as above) and then use the VS2010 designer to setup the binding source from the properties pane. Essentially, this is the only way I know of to bind an inner control's properties to the base control. If there's a better way to do it, please let me know.

除了 Label 和 ComboBox ItemsSource 上的绑定之外,Xaml 非常简单。我发现使这些绑定正确的最简单方法是在 .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

The XAML to use this is not interesting at all.. which is exactly what I wanted. You can set the ItemsSource and the LabelContent via all the standard WPF techniques.

使用它的 XAML 一点都不有趣......这正是我想要的。您可以通过所有标准 WPF 技术设置 ItemsSource 和 LabelContent。

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

For Completeness Sake, here is the MainWindow.xaml.cs

为了完整起见,这里是 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();

    }
}

回答by Jakub Pawlinski

I tried your solution but it fails for me. It does not pass the value over to inner control at all. What I did is declaration of same dependency properties in outer control and bound inner to outer like that:

我尝试了您的解决方案,但对我来说失败了。它根本不将值传递给内部控制。我所做的是在外部控件中声明相同的依赖属性并将内部绑定到外部,如下所示:

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

Than in xaml:

比在 xaml 中:

  <UserControl x:Class="CBRControls.TimePickerEx" x:Name="TimePickerExControl"
        ...
        >

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

  </UserControl>