Introduction
During a system development and its implementation it became a need to Bind some properties of a class representing an Entity from a DB to a wrapper class used as a View Model.
One way to accomplish it was to add a Handler to a PropertyChanged
event
and in the handler compare the name of that property; if the name
is the one looked for then to do something, of course some cheap work around can be made to update the source property in case the target was changed, but this is simpler.
Any how, let's consider that the source class has lots of properties. In such a case the PropertyChnaged
event
may be fired quite frequently
and in the handler a switch case
statement or many if
statements will be needed to filter the handler for the corresponding
property and it is not such a pretty implementation.
The wrong way, in my opinion, to do it presented in the next code snippet
private void PropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch(e.PropertyName)
{
case "property1":
{
break;
}
case "property2":
{
break;
}
}
}
In this article presented a mechanism for binding properties selectively and similar to the way implemented by the WPF. in WPF for example almost each GUI element
has a Method Set
Binding(DependencyProperty, Binding)
. but a Class that implements interface such as INotifyPropertyChanged
but not derived
from some GUI class such as those in Windows Presentation Framework (WPF)
are lacking of such mechanism as SetBinding(DependencyProperty, Binding)
that enables binding of properties.
Background
Each class that wants to notify all that interested in the fact that one of it's properties has changed has to implement interface such as INotifyPropertyChanged
.
The standard implementation of such an Interface is as follows:
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
#endregion
Two most common ways using the implementation of INotifyPropertyChanged
in the code are:
private string m_Name;
public string Name
{
get { return m_Name; }
set
{
m_Name=value;
if (OnPropertyChanged!=null)
{
OnPropertyCanged("Name");
}
}
}
public string Name
{
get { return m_Name; }
set
{
if (m_Name!=value)
{
m_Name=value;
if (OnPropertyChanged!=null)
{
OnPropertyCanged("Name");
}
}
}
}
Typical way to sign for notifications of property change is:
OnPropertyChanged += new PropertyChangedEventHandler(PropertyChanged);
By invoking the event OnPropertyCanged("Name")
every handler signed for the event
OnPropertyChanged
will be notified.
So far what was described are the mechanics to create and use properties, to understand more deeply the mechanics of Properties I suggest
to start here and just travel the links.
In the WPF
there is already built in mechanics for properties binding either through Xaml
or in the code behind, and for
it some parameters of binding needed to be defined such as Binding Mode (Two Way, One Way To Source, One Way To Target, etc.), the source and the target objects for binding
and their properties to be bounded, the update trigger and etc. but as mentioned before that method is mechanism is accessible only for the GUI elements and controls,
but what if there is a requirement to bind properties of not GUI elements and controls?
Code and Answer
Well as an answer for previously asked question next code is provided:
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
}
}
This code enables binding properties of classes that implement interfaces such as INotifyPropertyChanged and not necessary a derivation of some GUI element or Controls.
The solution consists of two classes: CLRBinding
and CLRPropertiesBinding
. and enum CLRBindingMode
.
CLRBindingMode
As the name suggests, defines the directions of the notification.
If the Binding Mode set to TwoWay
then on change of Source the Target will be Updated and like wise on change
of Target the Source will be Updated, if the Binding Mode set to OneWayToTarget
then
in case Source was changed the Target will be Updated but in case of Target change the Source
will remain with no change and finally if the Binding Mode set to OneWayToSource
then in case Target was changed the Source
will be Updated but in case of Source change the Target will remain with no change.
CLRPropertiesBinding
Represents the binding of two properties of two object, in case the Binding Mode is TwoWay
then both Classes should implement INotifyPropertyChanged
.
The CLRPropertiesBinding Class has two main Constructors:
public CLRPropertiesBinding(object source, string sourceProperty, object target,
string targetProperty, CLRBindingMode mode, IValueConverter converter )
This constructor receives as parameters two objects and the name of the properties to be bounded, but what is more important are the two last parameters:
- CLRBindingMode - which was covered previously but important to notice that it has default value of
TwoWay
.
- IValueConverter - which allows to bind properties of different types by implementing the
IValueConverter
Interface,
for more information click here.
The second constructor is:
public CLRPropertiesBinding(object source, object target, string property,
CLRBindingMode mode,IValueConverter converter) : this(source, property, target, property, mode, converter) { }
Only difference between them is that the second one used if the name of the Source property is the same as the name of the Target property.
CLRBinding
This is just some simple Class that manages the bindings, the ideas is that for each pair of Classes there is a single instance of CLRBinding
that manages and contains many instances of CLRPropertyBinding
where each CLRPropertyBinding
represents a pair of two properties bounded toghether.
Using the code
Well to use the code is always simpler to explain by an example. lets consider two Classes: Class1 and Class2, both implementing INotifyPropertyChanged as follows:
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
}
For the example to be more interesting notice that in Class1 the property StringCount
is of type string and in Class2
the property IntCount
is of type int, and for binding them together the following see the following example of the IValueConverter
implementation.
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
}
Important to notice that in IValueConverter the Method Convert invoked upon target update and ConvertBack upon Source Update.
Now that all components are implemented the following code snippet actually demonstrates the the use with a simple main method.
static void Main(string[] args)
{
Class1 class1 = new Class1();
Class2 class2 = new Class2();
StringToIntConverter converter = new StringToIntConverter();
CLRBinding.CLRBinding binding = new CLRBinding.CLRBinding();
CLRPropertiesBinding PropertyBinding = new CLRPropertiesBinding(class2,
"IntCount", class1, "StringCount", CLRBindingMode.TwoWay, converter);
binding.Add(PropertyBinding);
binding.Bind();
class1.StringCount = "100";
class2.IntCount = 50;
}
Possible Improvements
Both methods of IValueConverter
contains in addition of the value and type two more parameter, the CultureInfo
, and parameter
.
Currently both are not quite in use, the CultureInfo
always set to CurrentCulture
and the parameter
is always null
,
if there will be a need for these parameters it would be a possible impprovement.