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






4.78/5 (28投票s)
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 提供的功能。
|
|
BindingHub 之前:每个 DependencyProperty 只有一个 Binding
|
BindingHub 像电源插排:将多个 Bindings 连接到同一个 DependencyProperty(OneWay、TwoWay,如有必要使用 Converters)
|
|
|
BindingHub 之前:每个 DependencyProperty 只有一个 Binding
|
BindingHub 像电话交换机:路由、连接、多路复用、轮询数据、推送数据、转换数据
|
|
|
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、强制转换、属性更改处理程序(包含 OldValue
和 NewValue
,以便您可以正确地释放或断开未使用的组件)等等。您可以声明自定义的 OnCoerce
和 OnChange
处理程序,并将它们附加到相应的 BindingHub 事件。
通过附加/分离不同的预定义策略和验证器(或者作为最后的手段,通过重新布线和重新绑定 BindingHub 中的插槽并附加 OnChange
/ OnCoerce
事件处理程序),您可以即时更改 ViewModel 的行为。软件模式,来了。
通过将验证、强制转换和状态更改逻辑分离到单独的组件中,您可以消除令人头疼的“意大利面条”代码,以及散布在 ViewModel 中众多 getter 和 setter 中的隐藏依赖关系和陷阱。
代码隐藏/ViewModel 编程将变得更加声明式,更像其 WPF 的对应物,而不是像今天这样一团糟。
用例
连接器
<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 才能作为绑定的目标。
多路复用器
<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,瞧:您的属性正在被神奇地验证。
计算器
<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 = true,Input 将被复制到 Output。
待办事项:使用 Python 脚本进行条件复制操作(以消除对自定义 OnCoerce
处理程序的需求)。
条件绑定
评论:同样,您的想象力是唯一的限制。
待办事项:使用 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);
}
};
}
}
策略模式
评论:通过设置 DataContext = null 来分离 Strategy1,附加 Strategy2。
轮询/推送数据
待办事项:在 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}
动画器
<!-- 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 解决的典型任务:使用带 GridLengthValueConverter
的 Int32Animation
动画 ColumnDefinition.Width
或 RowDefinition.Height
,使用带 BrushValueConverter
的 ColorAnimation
动画 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 日 - 向源代码和文章添加了动画。