CLRBinding - 属性绑定
绑定非 GUI 元素的属性,这些元素仍然实现 INotify。
引言
在系统开发及其实现过程中,需要将表示数据库实体的一个类的某些属性绑定到一个用作视图模型的包装类。
完成此操作的一种方法是向 PropertyChanged
事件
添加一个处理程序,并在处理程序中比较该属性的名称;如果名称是所需的,则执行某些操作,当然,在目标属性被更改时,也可以进行一些简单的变通来更新源属性,但这更简单。
无论如何,考虑到源类有很多属性。在这种情况下,PropertyChnaged
事件
可能会频繁触发,而在处理程序中需要一个 switch case
语句或多个 if
语句来过滤与相应属性匹配的处理程序,这并不是一个很漂亮的实现。
在我看来,用错误的方式来实现它,如下面的代码片段所示。
private void PropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch(e.PropertyName)
{
case "property1":
{
//do something
break;
}
case "property2":
{
//do something
break;
}
//and so on ...
}
}
本文介绍了一种选择性绑定属性的机制,类似于 WPF 的实现方式。例如,在 WPF 中,几乎每个 GUI 元素都有一个 Set
Binding(DependencyProperty, Binding)
方法。但是,一个实现了 INotifyPropertyChanged
等接口但没有继承某些 GUI 类(如 Windows Presentation Framework (WPF)
中的类)的类缺乏 SetBinding(DependencyProperty, Binding)
这样的机制,该机制可以实现属性的绑定。
背景
每个希望通知所有关心其某个属性已更改的类都必须实现 INotifyPropertyChanged
等接口。此接口的标准实现如下:
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
#endregion
在代码中使用 INotifyPropertyChanged
实现的两种最常见方式是:
private string m_Name;
public string Name
{
get { return m_Name; }
set
{
m_Name=value;
if (OnPropertyChanged!=null)
{
OnPropertyCanged("Name");
}
}
}
// or
public string Name
{
get { return m_Name; }
set
{
if (m_Name!=value)
{
m_Name=value;
if (OnPropertyChanged!=null)
{
OnPropertyCanged("Name");
}
}
}
}
注册属性更改通知的典型方式是:
OnPropertyChanged += new PropertyChangedEventHandler(PropertyChanged);
通过调用事件 OnPropertyCanged("Name")
,所有注册到 OnPropertyChanged
事件
的处理程序都将收到通知。
到目前为止所描述的都是创建和使用属性的机制,要更深入地理解属性的机制,我建议从 这里 开始,然后随意浏览链接。
在 WPF
中,已经内置了属性绑定的机制,可以通过 Xaml
或代码隐藏来实现,并且为此需要定义一些绑定参数,例如绑定模式(双向、仅源到目标、仅目标到源等)、绑定源和目标对象以及它们要绑定的属性、更新触发器等。但如前所述,该方法只对 GUI 元素和控件可用,如果需要绑定非 GUI 元素和控件的属性怎么办?
代码与答案
好吧,作为对之前问题的回答,提供了以下代码:
namespace CLRBinding
{
public class CLRBinding
{
List<clrpropertiesbinding> Bindings = new List<clrpropertiesbinding>();
public void Add(CLRPropertiesBinding binding)
{
Bindings.Add(binding);
}
public void Remove(CLRPropertiesBinding binding)
{
Bindings.Remove(binding);
binding.Unbind();
}
public void Bind()
{
foreach (var binding in Bindings)
{
binding.Bind();
}
}
}
}
namespace CLRBinding
{
public enum CLRBindingMode
{
OneWayToTarget,
OneWayToSource,
TwoWay
}
public class CLRPropertiesBinding
{
#region Memebers
private readonly CLRBindingMode m_Mode;
private readonly object m_Source;
private readonly object m_Target;
private readonly PropertyDescriptor m_SourceProperty;
private readonly PropertyDescriptor m_TargetProperty;
private readonly IValueConverter m_Converter;
#endregion
#region Constructors
public CLRPropertiesBinding(object source, string sourceProperty, object target,
string targetProperty, CLRBindingMode mode, IValueConverter converter )
{
if (source == null)
{
throw new ArgumentNullException("source");
}
if (target == null)
{
throw new ArgumentNullException("target");
}
PropertyDescriptorCollection sourceProperties = TypeDescriptor.GetProperties(source);
m_SourceProperty = sourceProperties.Find(sourceProperty, false);
if (m_SourceProperty == null)
{
throw new ArgumentException("Invalid source property");
}
if ((mode == CLRBindingMode.TwoWay || mode == CLRBindingMode.OneWayToSource) && m_SourceProperty.IsReadOnly)
{
throw new ArgumentException("TwoWay and OneWayToSource are not supported on a readonly source property");
}
PropertyDescriptorCollection targetProperties = TypeDescriptor.GetProperties(target);
m_TargetProperty = targetProperties.Find(targetProperty, false);
if (m_TargetProperty == null)
{
throw new ArgumentException("Invalid target property");
}
if (mode != CLRBindingMode.OneWayToSource && m_TargetProperty.IsReadOnly)
{
throw new ArgumentException("TwoWay and OneWayToTarget are " +
"not supported on a readonly target property");
}
m_Source = source;
m_Target = target;
m_Mode = mode;
m_Converter = converter;
}
public CLRPropertiesBinding(object source, string sourceProperty, object target,
string targetProperty, IValueConverter converter = null) :
this(source, sourceProperty, target, targetProperty, CLRBindingMode.TwoWay, converter) { }
public CLRPropertiesBinding(object source, string sourceProperty, object target,
string targetProperty, CLRBindingMode mode = CLRBindingMode.TwoWay) :
this(source, sourceProperty, target, targetProperty, CLRBindingMode.TwoWay, null) { }
public CLRPropertiesBinding(object source, string sourceProperty, object target,
string targetProperty) : this(source, sourceProperty, target,
targetProperty, CLRBindingMode.TwoWay, null) { }
public CLRPropertiesBinding(object source, object target, string property,
CLRBindingMode mode, IValueConverter converter) : this(source, property,
target, property, mode, converter) { }
public CLRPropertiesBinding(object source, object target, string property,
CLRBindingMode mode) : this(source, target, property, mode, null) { }
public CLRPropertiesBinding(object source, object target, string property,
IValueConverter converter) : this(source, target, property, CLRBindingMode.TwoWay, converter) { }
public CLRPropertiesBinding(object source, object target, string property) :
this(source, target, property, CLRBindingMode.TwoWay, null) { }
#endregion
#region Operations
internal void Bind()
{
if (m_Mode == CLRBindingMode.OneWayToTarget || m_Mode == CLRBindingMode.TwoWay)
{
m_SourceProperty.AddValueChanged(m_Source, SourcePropertyChanged);
}
if (m_Mode == CLRBindingMode.OneWayToSource || m_Mode == CLRBindingMode.TwoWay)
{
m_TargetProperty.AddValueChanged(m_Target, TargetPropertyChanged);
}
UpdateTarget();
}
internal void Unbind()
{
if (m_Mode == CLRBindingMode.OneWayToTarget || m_Mode == CLRBindingMode.TwoWay)
{
m_SourceProperty.RemoveValueChanged(m_Source, SourcePropertyChanged);
}
if (m_Mode == CLRBindingMode.OneWayToSource || m_Mode == CLRBindingMode.TwoWay)
{
m_TargetProperty.RemoveValueChanged(m_Target, TargetPropertyChanged);
}
}
#endregion
#region Privates
private void SourcePropertyChanged(object sender, EventArgs e)
{
UpdateTarget();
}
private void UpdateTarget()
{
object value = m_SourceProperty.GetValue(m_Source);
m_TargetProperty.RemoveValueChanged(m_Target, TargetPropertyChanged);
if (m_Converter != null)
{
value = m_Converter.Convert(value, m_TargetProperty.GetType(), null, CultureInfo.CurrentCulture);
}
m_TargetProperty.SetValue(m_Target, value);
m_TargetProperty.AddValueChanged(m_Target, TargetPropertyChanged);
}
private void TargetPropertyChanged(object sender, EventArgs e)
{
UpdateSource();
}
private void UpdateSource()
{
object value = m_TargetProperty.GetValue(m_Target);
m_SourceProperty.RemoveValueChanged(m_Source, SourcePropertyChanged);
if (m_Converter != null)
{
value = m_Converter.ConvertBack(value, m_TargetProperty.GetType(), null, CultureInfo.CurrentCulture);
}
m_SourceProperty.SetValue(m_Source, value);
m_SourceProperty.AddValueChanged(m_Source, SourcePropertyChanged);
}
#endregion
}
}
此代码实现了对实现了 INotifyPropertyChanged
等接口的类的属性进行绑定,而不必是某些 GUI 元素或控件的派生类。
该解决方案包含两个类:CLRBinding
和 CLRPropertiesBinding
。以及一个 enum CLRBindingMode
。
CLRBindingMode
顾名思义,它定义了通知的方向。
如果绑定模式设置为 TwoWay
,则在源更改时,目标将被更新;同样,在目标更改时,源将被更新。如果绑定模式设置为 OneWayToTarget
,则在源更改时,目标将被更新,但在目标更改时,源将保持不变。最后,如果绑定模式设置为 OneWayToSource
,则在目标更改时,源将被更新,但在源更改时,目标将保持不变。
CLRPropertiesBinding
表示两个对象两个属性的绑定,如果绑定模式是 TwoWay
,则两个类都必须实现 INotifyPropertyChanged
。
CLRPropertiesBinding
类有两个主要的构造函数。
public CLRPropertiesBinding(object source, string sourceProperty, object target,
string targetProperty, CLRBindingMode mode, IValueConverter converter )
此构造函数接收两个对象和要绑定的属性名称作为参数,但更重要的是最后两个参数:
- CLRBindingMode - 之前已经介绍过,但重要的是要注意它有一个默认值
TwoWay
。 - IValueConverter - 通过实现
IValueConverter
接口,允许绑定不同类型的属性。有关更多信息,请单击 此处。
第二个构造函数是:
public CLRPropertiesBinding(object source, object target, string property,
CLRBindingMode mode,IValueConverter converter) : this(source, property, target, property, mode, converter) { }
它们之间唯一的区别是,当源属性的名称与目标属性的名称相同时,使用第二个构造函数。
CLRBinding
这只是一个简单的类,用于管理绑定。其思想是,对于每对类,都有一个 CLRBinding
的实例,该实例管理并包含许多 CLRPropertyBinding
的实例,其中每个 CLRPropertyBinding
代表一对一起绑定的属性。
使用代码
好吧,为了让代码使用更易于解释,最好通过一个例子。让我们考虑两个类:Class1 和 Class2,它们都实现 INotifyPropertyChanged,如下所示:
class Class1 : INotifyPropertyChanged
{
private string m_Count;
public string StringCount
{
get { return m_Count; }
set
{
if (m_Count!=value)
{
m_Count=value;
OnPropertyChanged("StringCount");
}
}
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
#endregion
}
class Class2 : INotifyPropertyChanged
{
private int m_Count;
public int IntCount
{
get { return m_Count; }
set
{
if (m_Count!=value)
{
m_Count=value;
OnPropertyChanged("IntCount");
}
}
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
#endregion
}
为了使示例更有趣,请注意 Class1 中的 StringCount
属性是 string 类型,而 Class2
中的 IntCount
属性是 int 类型。为了将它们绑定在一起,请参阅以下 IValueConverter
实现的示例。
class StringToIntConverter :IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
int _value = (int)value;
return _value.ToString();
}
public object ConvertBack(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
string _value = (string)value;
int _IntValue;
return (int.TryParse(_value, out _IntValue)) ? _IntValue : 0;
}
#endregion
}
重要的是要注意,在 IValueConverter 中,Convert 方法在目标更新时调用,ConvertBack 方法在源更新时调用。
现在所有组件都已实现,以下代码片段实际上演示了如何使用一个简单的 main 方法。
static void Main(string[] args)
{
//instantiate the classes to be bound
Class1 class1 = new Class1();
Class2 class2 = new Class2();
// instantiate the converter
StringToIntConverter converter = new StringToIntConverter();
// create the binding class that managing the group of ProipertyBinding
CLRBinding.CLRBinding binding = new CLRBinding.CLRBinding();
// instantiate the property binding between object class1 as target and
// class2 as source, between their properties StringCount and IntCount
CLRPropertiesBinding PropertyBinding = new CLRPropertiesBinding(class2,
"IntCount", class1, "StringCount", CLRBindingMode.TwoWay, converter);
// add the PropertyBinding to the Binding class
binding.Add(PropertyBinding);
//activate the binding
binding.Bind();
// the binding is TwoWay hence to it change in one will change the other
class1.StringCount = "100";
class2.IntCount = 50;
}
可能的改进
IValueConverter
的两种方法除了值和类型外,还有两个参数:CultureInfo
和 parameter
。
目前两者都不是很常用,CultureInfo
始终设置为 CurrentCulture
,parameter
始终为 null
。如果需要这些参数,则可以进行改进。