65.9K
CodeProject 正在变化。 阅读更多。
Home

BindingHub - 一个新的组件和设计模式,在 WPF 以及 ViewModels 中都非常有用

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (28投票s)

2010 年 12 月 27 日

Eclipse

7分钟阅读

viewsIcon

87286

downloadIcon

520

BindingHub 是自切片面包以来最伟大的发明。读完本文后,您会开始想知道自己是如何在没有 BindingHub 的情况下度过如此漫长的时间。在我创建它之后,我也曾有过同样的疑问。

你为什么需要 BindingHub?

在深入探讨具体的用例和实现细节之前,让我们先看看当前 WPF 实现中缺少了什么。

WPF 被设计为声明式编程,所以所有的代码都必须写在 Code-Behind / ViewModel 中。不幸的是,任何非简单的 ViewModel 很快就会变成“意大利面条”(Spaghetti with Meatballs,如果不明白请查阅维基百科),充斥着大量不必要的属性 getter 和 setter,存在隐藏的依赖关系和陷阱。

您需要在 IsClientActive == True 时显示某些内容,然后在 IsClientActive == False 时显示另一些内容,接着需要 IsClientActive && IsAccountOpen,然后需要 IsClientActive || not IsAccountOpen,等等。ViewModel 的属性数量像滚雪球一样增长,它们之间以复杂的方式相互依赖,每次需要显示/隐藏/折叠/改变颜色/等等,您都必须创建越来越多的属性,并重新编译和重新测试您的 ViewModel。

另一种选择是使用 Triggers,但它们只允许在 DataTemplates 或 ControlTemplates 中使用。此外,您无法在其他地方重用相同的 Trigger,因此您必须复制代码和匹配逻辑的整个列表。无论如何,很多时候,您根本无法用 Triggers 表达必要的逻辑(例如,IsClientActive || OrderCount > 0)。

另一种选择是使用 ValueConverters 和 MultiBindings,但 MultiBindings 的使用非常不方便:您无法内联定义它们,无法在其他地方重用相同的 MultiBinding,每次都需要创建另一个 ValueConverter,而且它也非常容易出错。有一些助手,如 ConverterChain(可以将多个转换器组合起来),等等,但它们并没有消除上述所有问题。

很多时候,您需要将一个 DependencyProperty 绑定到多个控件和/或多个 ViewModel 属性。例如,Slider.Value 可以绑定到 Chart 控件的 Zoom 属性,但您也希望在其他地方的 TextBlock 中显示相同的值,并在您的 ViewModel.Zoom 属性中记录该值。运气不佳,因为 Slider.Value 属性只能有一个 Binding,所以您必须费尽周折,创建一些变通办法(使用 Tag 属性、隐藏字段,等等)……

有时您需要一个属性根据条件更新,或者当另一个属性触发更新时更新,或者您需要开启/关闭更新……

回想一下,您有多少次迫切需要那个额外的 Binding、那个额外的 DependencyProperty、那个额外的 Trigger、那个额外的逻辑表达式……

BindingHub 来救援

让我们从宏观角度看看 BindingHub 提供的功能。

electric-socket-single.JPG

power-strip-connected.JPG

BindingHub 之前:每个 DependencyProperty 只有一个 Binding
BindingHub 像电源插排:将多个 Bindings 连接到同一个 DependencyProperty(OneWay、TwoWay,如有必要使用 Converters)

phones.JPG

switch-board.JPG

BindingHub 之前:每个 DependencyProperty 只有一个 Binding
BindingHub 像电话交换机:路由、连接、多路复用、轮询数据、推送数据、转换数据

spaghetti.JPG

circuit-board.JPG

BindingHub 之前:代码隐藏/ViewModel 中的意大利面条代码
BindingHub 像电子电路板:在插槽之间布线连接,连接/断开预制组件

在 ViewModels 中的用法

很多时候,您需要在一个变量改变时执行一些计算

OrderQty = FilledQty + RemainingQty;

或者在某个属性改变时对其进行验证

if (TextLength == 0)
     ErrorMsg = "Text cannot be empty";

或者您需要计算单词的数量

WordCount = CalculateWordCount(Text);

也许您需要在某个属性改变时释放某个对象,或者在另一个属性改变时填充一个集合,或者连接/断开某些东西,或者在 MaxValue 改变时强制 MinValue……动画会很好,样式和皮肤也会很好。

所有这些操作都可以通过 DependencyProperties 轻松完成,所以如果我们能让我们的 ViewModel 继承自 DependencyObject,那么实现任何类型的强制转换或属性更改处理将非常快速。

不幸的是,您很少有机会为 ViewModel 选择任意的基类(通常已经有一个您必须使用的基类),所以继承自 DependencyObject 是不可能的。您必须在 getter 和 setter 中实现所有的强制转换和属性更改处理,复杂性和隐藏的依赖关系会很快失控,并且您会很快得到“意大利面条”代码。

好了,BindingHub 来救援。

您可以创建 ViewModel 类,其中包含简单的 getter、setter 和 NotifyPropertyChanged,如下所示

public class ViewModel : INotifyPropertyChanged
{
    private string _text = "Hello, world";
    public string Text
    {
        get { return _text; }
        set { _text = value; OnPropertyChanged("Text"); }
    }
    private int _textLength;
    public int TextLength
    {
        get { return _textLength; }
        set { _textLength = value; OnPropertyChanged("TextLength"); }
    }
    private int _wordCount;
    public int WordCount
    {
        get { return _wordCount; }
        set { _wordCount = value; OnPropertyChanged("WordCount"); }
    }
    private string _errorMsg;
    public string ErrorMsg
    {
        get { return _errorMsg; }
        set { _errorMsg = value; OnPropertyChanged("ErrorMsg"); }
    }
    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged(string property)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(property));
        }
    }
}

好了,各位。现在,继续进行更有趣的内容。

在运行时,您可以附加一个或多个 BindingHub 创建的预定义策略(Strategies)、验证器(Validators)和计算器(Calculators),瞧!您的所有属性都会神奇地表现得就像它们一直是 DependencyProperties 一样。您将能够使用 Bindings、MultiBindings、OneWay、TwoWay、OneWayToSource、Value Converters、强制转换、属性更改处理程序(包含 OldValueNewValue,以便您可以正确地释放或断开未使用的组件)等等。您可以声明自定义的 OnCoerceOnChange 处理程序,并将它们附加到相应的 BindingHub 事件。

通过附加/分离不同的预定义策略和验证器(或者作为最后的手段,通过重新布线和重新绑定 BindingHub 中的插槽并附加 OnChange / OnCoerce 事件处理程序),您可以即时更改 ViewModel 的行为。软件模式,来了。

通过将验证、强制转换和状态更改逻辑分离到单独的组件中,您可以消除令人头疼的“意大利面条”代码,以及散布在 ViewModel 中众多 getter 和 setter 中的隐藏依赖关系和陷阱。

代码隐藏/ViewModel 编程将变得更加声明式,更像其 WPF 的对应物,而不是像今天这样一团糟。

用例

连接器

connector.GIF

<bh:BindingHub Name="Connector"
      Socket1="{Binding SelectedItems, ElementName=Grid, Mode=OneWay}" 
      Socket2="{Binding SelectedItems, Mode=OneWayToSource}" 
      Connect="(1 in, 2 out)" >
</bh:BindingHub>

评论:看起来很简单吗?是的,当然,因为它很简单,但试着将 XamDataGrid.SelectedItems(不是 DependencyProperty)绑定到 ViewModel.SelectedItems(不是 DependencyProperty)。哎呀,只有 DependencyProperty 才能作为绑定的目标。

多路复用器

multiplexor.GIF

<bh:BindingHub Name="Multiplexor" 
    Socket1="{Binding Text, ElementName=Input}" 
    Socket2="{Binding PlainText, Mode=OneWayToSource}" 
    Socket3="{Binding WordCount, 
            Converter={StaticResource WordCountConverter}, Mode=OneWayToSource}" 
    Socket4="{Binding SpellCheck, 
            Converter={StaticResource SpellCheckConverter}, Mode=OneWayToSource}" 
    Connect="(1 in, 2 out, 3 out, 4 out)" >
</bh:BindingHub>

验证器

public class Validator : BindingHub
{
    public Validator()
    {
        SetBinding(Socket1Property, new Binding("TextLength")
            { Mode = BindingMode.OneWay });
        SetBinding(Socket2Property, new Binding("ErrorMsg") 
            { Mode = BindingMode.OneWayToSource });
        Socket1Changed += (s, e) =>
        {
            Socket2 = (int)e.NewValue == 0 ? "Text cannot be empty" : "";
        };
    }
}

评论:您只需设置 DataContext 即可将 Validator 附加到您的 ViewModel,瞧:您的属性正在被神奇地验证。

计算器

calculator.GIF

<bh:BindingHub Name="Calculator" 
        Socket1="{Binding Text, ElementName=Input_1}" 
        Socket2="{Binding Text, ElementName=Input_2}" 
        Socket3="{Binding Text, ElementName=Output}" 
        Connect="(4 in, 3 out)" >
    <bh:BindingHub.Socket4>
        <MultiBinding Converter="{StaticResource AddConverter}">
            <Binding RelativeSource="{RelativeSource Self}" Path="Socket1"/>
            <Binding RelativeSource="{RelativeSource Self}" Path="Socket2"/>
        </MultiBinding>
    </bh:BindingHub.Socket4>
</bh:BindingHub>

评论:您可以计算总和、角度(例如,用于模拟时钟显示)、比例……您的想象力是唯一的限制。

待办事项:在 Value Converters 中使用 Python 脚本。

触发器属性

trigger.GIF

评论:设置 Trigger = true,Input 将被复制到 Output。

待办事项:使用 Python 脚本进行条件复制操作(以消除对自定义 OnCoerce 处理程序的需求)。

条件绑定

conditional.GIF

评论:同样,您的想象力是唯一的限制。

待办事项:使用 Python 脚本进行条件复制操作(以消除对自定义 OnChange 处理程序的需求)。

附加/分离/分配/释放模式

public class Helper : BindingHub
{
   public Helper()
   {
      SetBinding(Socket1Property, new Binding("SomeResource")
              { Mode = BindingMode.OneWay });

      Socket1Changed += (s, e) =>
      {
          if (e.OldValue != null)
          {
              ((Resource)e.OldValue).Dispose(); // Or parent.Detach(e.OldValue);
          }
          if (e.NewValue != null)
          {
              ((Resource)e.NewValue).Allocate(); // Or parent.Attach(e.NewValue);
          }
      };
   }
}

策略模式

strategy.GIF

评论:通过设置 DataContext = null 来分离 Strategy1,附加 Strategy2。

轮询/推送数据

polling.GIF

待办事项:在 BindingHub 内部实现定时更新。

交换板/电路板/草稿板

<!--used as a scratch pad to keep various converted/calculated properties-->
<bh:BindingHub Name="ScratchPad"
    Socket1="{Binding IsClientActive, 
                  Converter={StaticResource NotConverter}}" 
    Socket2="{Binding Text}" 
    Socket3="{Binding TextLength}" 
    Socket4="{Binding ErrorMsg}" 
    Socket5="{Binding Socket3, ElementName=Calculator1, Mode=OneWay}" 
    Socket6="{Binding ElementName=TheTextBox, Mode=OneWay}"
    Socket7="{Binding TextBoxItself, Mode=OneWayToSource}" 
    Socket8="{Binding Text, ElementName=TheTextBox}" 
    Socket9="{Binding Title, ElementName=Main, Mode=OneWayToSource}" 
    Connect="(6 in, 7 out),(8 in, 9 out)" >
</bh:BindingHub>

链式 BindingHubs

<!-- if 16 sockets are not enough, you can chain BindingHubs -->
<bh:BindingHub Name="FirstHub"
    Socket1="1" 
    Socket2="2" 
    Socket3="{Binding Extension.Socket3,
        RelativeSource={RelativeSource Self}}">
        <bh:BindingHub Name="SecondHub"
            Socket1="{Binding Parent.Socket1,
                RelativeSource={RelativeSource Self}}"
            Socket2="2" 
            Socket3="{Binding Extension.Socket3, 
                RelativeSource={RelativeSource Self}}">
                <bh:BindingHub Name="ThirdHub"
                    Socket1="{Binding Parent.Socket1,
                        RelativeSource={RelativeSource Self}}"
                    Socket2="2" 
                    Socket3="3">
                </bh:BindingHub>
        </bh:BindingHub>
</bh:BindingHub>

BindingHub 附加属性

<Window Name="MainWindow">
    <bh:BindingHub.BindingHub>
        <bh:BindingHub 
            Socket1="{Binding ABC}"
            Socket2="{Binding Title, ElementName=MainWindow}"
            Socket3="Some string" />
    </bh:BindingHub.BindingHub>

    <!-- the whole bunch of elements can go there -->

    <TextBox Text="{Binding (bh:BindingHub.BindingHub).Socket1, 
            RelativeSource={RelativeSource Self}}"/>
    <TextBox Text="{Binding (bh:BindingHub.BindingHub).Socket2, 
            RelativeSource={RelativeSource Self}}"/>
</Window>

评论:将 BindingHub 属性视为 DataContext 概念的扩展。就像 DataContext 一样,它是可继承的,所以您可以将其设置在父级上,所有子级和孙级都会继承它。

注意事项:当 BindingHub 附加到元素(此处为 Window)时,它**不会**像往常一样连接到逻辑树(它是逻辑树的一个虚拟分支)。为了使用 ElementName Binding,我不得不将 BindingHub 的 NameScope 属性绑定到父元素的 NameScope 属性(DataContext 也已绑定)。父级**必须**设置 Name(或 x:Name),否则其 NameScope 将保持为空,并且 ElementName Binding 将不起作用。

待办事项:创建 HubBinding 扩展,以便代替

{Binding (bh:BindingHub.BindingHub).Socket2, RelativeSource={RelativeSource Self}}

您将能够简单地说

{bh:HubBinding Socket2}

动画器

animator.GIF

<!-- used to animate both ViewModel and WPF properties -->
<bh:BindingHub Name="Animator"
      Socket1="{Binding WordCount, Mode=OneWay}" 
      Socket1Changed="Animator_Socket1Changed"
      Socket2="{Binding Background, ElementName=MainWindow, 
               Converter={StaticResource BrushConverter}, 
               Mode=OneWayToSource}"
      Connect="(Color in, Socket2 out)" >
                
      <bh:BindingHub.Resources>
            <Storyboard x:Key="Animation" >
                <!-- fancy background effect -->
                <ColorAnimation
                     Storyboard.TargetName="Animator"
                     Storyboard.TargetProperty="Color"
                     Duration="0:0:1" From="White" To="Plum"
                     AutoReverse="True" FillBehavior="HoldEnd" />
            </Storyboard>
      </bh:BindingHub.Resources>
</bh:BindingHub>
private void Animator_Socket1Changed(DependencyObject d,
        DependencyPropertyChangedEventArgs e)
{
    var animation = Animator.Resources["Animation"] as Storyboard;
    animation.Begin();
}

评论:是的,使用 BindingHub,您可以通过 Converters 动画 ViewModel 属性、x:Static 变量和 DependencyProperties,以及您能想象到的任何内容。您只能用 BindingHub 解决的典型任务:使用带 GridLengthValueConverterInt32Animation 动画 ColumnDefinition.WidthRowDefinition.Height,使用带 BrushValueConverterColorAnimation 动画 Background,使用 Int32Animation 动画 ViewModel.Progress 属性(不是 DependencyProperty!),等等。

待办事项:创建 EventTriggers 和 DataTriggers(可选使用 Python 脚本)以消除对 SocketxxxChanged 处理程序的需求。

源代码

该项目已在 SourceForge 上以 Eclipse Public License 发布,您可以在此处下载完整的源代码和一些用例示例:http://sourceforge.net/projects/bindinghub/

欢迎您为此贡献示例和新想法。

以下是一些尚未实现的想法

  • 脚本绑定
  • 脚本块
  • 脚本扩展
  • 脚本转换器
  • 只读属性和 NeedActual 触发器
  • 定时器激活绑定
  • 带转换器的绑定组
  • BindingHubExtension(创建 hub 并将其绑定到属性)
  • 带触发器属性的多重绑定

BindingHub 的源代码有点长而且枯燥(实现很简单,最有趣的部分是开始跳出框框思考并想出这个绝妙的主意),所以只需下载项目并玩一玩。

修订历史

  • 2010 年 12 月 27 日 - 创建了原始文章。
  • 2011 年 1 月 13 日 - 向源代码和文章添加了动画。
© . All rights reserved.