如何在用户控件中绑定多个属性

4
假设我们有这样一个UserControl:
<UserControl x:Class="...>
    <StackPanel>
        <TextBlock Name="TextBlock1" />
        <TextBlock Name="TextBlock2" />
        <TextBlock Name="TextBlock3" />
        ...
        <TextBlock Name="TextBlock10" />
    </StackPanel>
</UserControl>

我们有这样定义的属性:

public string Text1 { get; set; }
public string Text2 { get; set; }
public string Text3 { get; set; }
...
public string Text10 { get; set; }

现在我想将所有这些TextBlock绑定到所有这些属性。显然有多种方法可以做到这一点,我想知道它们的优缺点。让我们列出一张清单:

  1. My first approach was this:

    <TextBlock Name="TextBlock1" Text="{Binding Path=Text1, RelativeSource={RelativeSource AncestorType=UserControl}}" />
    

    This works and is fairly simple, but it is a lot of redundant code if I have to type it for all the TextBlocks. In this example we could just copy-paste, but the UserControl could be more complex.

  2. When I googled the problem I found this solution:

    <UserControl ...
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
        <TextBlock Name="TextBlock1" Text="{Binding Path=Text1}" />
    

    This looks pretty clean in the xaml, however DataContext could be used otherwise. So if someone uses this UserControl and changes the DataContext, we are screwed.

  3. Another common solution seems to be this:

    <UserControl ...
        x:Name="MyUserControl">
        <TextBlock Name="TextBlock1" Text="{Binding Path=Text1, ElementName=MyUserControl}" />
    

    This has the same problem as 2. though, Name could be set from somewhere else.

  4. What if we write our own MarkupExtension?

    public class UserControlBindingExtension : MarkupExtension
    {
        public UserControlBindingExtension() { }
    
        public UserControlBindingExtension(string path)
        {
            this.Path = path;
        }
    
        private Binding binding = null;
    
        private string path;
    
        [ConstructorArgument("path")] 
        public string Path
        {
            get
            {
                return path;
            }
            set
            {
                this.path = value;
                binding = new Binding(this.path);
                binding.RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(UserControl), 1);
            }
        }
    
        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            if(binding == null)
                return null;
    
            return binding.ProvideValue(serviceProvider);
        }
    }
    

    Now we can do something like this:

    <UserControl ...
        xmlns:self="clr-namespace:MyProject">
        <TextBlock Name="TextBlock1" Text="{self:UserControlBinding Path=Text1}"
    

    Neat! But I'm not conviced if my implementation is bulletproof and I would prefer not writing my own MarkupExtension.

  5. Similar to 4. we could do this:

    public class UserControlBindingHelper : MarkupExtension
    {
        public UserControlBindingHelper() { }
    
        public UserControlBindingHelper(Binding binding)
        {
            this.Binding = binding;
        }
    
        private Binding binding;
        [ConstructorArgument("binding")]
        public Binding Binding
        {
            get
            {
                return binding;
            }
            set
            {
                binding = value;
                binding.RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(UserControl), 1);
            }
        }
    
        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            if (binding == null)
                return null;
    
            return binding.ProvideValue(serviceProvider);
        }
    }
    

    Which would result in code like this:

    <TextBlock Name="TextBlock1" Text="{self:UserControlBindingHelper {Binding Text1}}" />
    
  6. We could do it in code!

    private void setBindingToUserControl(FrameworkElement element, DependencyProperty dp, string path)
    {
        Binding binding = new Binding(path);
        binding.RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(StateBar), 1);
        element.SetBinding(dp, binding);
    }
    

    And then we do this:

    setBindingToUserControl(this.TextBlock1, TextBlock.Text, "Text1");
    

    Fairly nice, but it makes the xaml harder to read, since the information about the bindings is missing now.

  7. What other good/interesting options are there?

那么在什么情况下应该选择哪种方法?我是否犯了错误?
具体问题如下:
所有这些方法都正确吗?有些方法优于其他方法吗?


1
你在第三点的结论是错误的。当你在某个地方使用UserControl时设置名称不会破坏ElementName绑定。个人而言,我更喜欢使用RelativeSource AncestorType绑定。 - Clemens
@Clemens 很好了解,谢谢您的评论。 - Tim Pohlmann
2个回答

2

看起来你在这里做了很多好的测试!就像你可能已经注意到的那样,大多数方法都是可行的,但我建议使用您的第一种方法。虽然它可能看起来有点重复,但非常清晰且相对简单易于维护。这也使得您的代码易于被不太熟悉xaml的任何人阅读。

你已经知道解决方案2和3的问题。创建自己的标记扩展有点过度,至少在这种情况下,会使您的代码更难理解。解决方案6可以正常工作,但如你所说,无法在xaml中知道文本块(bind)到什么。


1
好答案!即使我的答案与你的竞争,我也投了赞成票 :) 欢迎来到StackOverflow! - Contango

0

只需使用#2,并假设DataContext是正确的。这是标准。使用用户控件的人有责任确保DataContext设置正确。

其他任何操作都会增加不必要的复杂性。

请参见{{link1:ReSharper WPF错误:“由于未知的DataContext而无法解析符号“MyVariable”}}。

对于任何使用用户控件的人来说,将DataContext设置为RelativeSource Self是常见做法,请参见{{link2:如何绑定到RelativeSource Self?}}


2
谢谢提供链接。你的第二个链接实际上有一个答案,特别建议避免在UserControl中设置DataContext。 - Tim Pohlmann

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