System.ComponentModel has a default implementation of ICustomTypeDescriptor. This provides the interface implementation requirements in the form of virtual methods which can be overridden in your custom type. We will be using this implementation as part of our discussion. Basically the following methods from the interface do all the magic. We can provide implementation of these methods to introduce our custom properties in order to fake the client that the type already has these properties.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Summary: | |
// Returns the properties for this instance of a component. | |
// | |
// Returns: | |
// A System.ComponentModel.PropertyDescriptorCollection that represents the | |
// properties for this component instance. | |
PropertyDescriptorCollection GetProperties(); | |
// | |
// Summary: | |
// Returns the properties for this instance of a component using the attribute | |
// array as a filter. | |
// | |
// Parameters: | |
// attributes: | |
// An array of type System.Attribute that is used as a filter. | |
// | |
// Returns: | |
// A System.ComponentModel.PropertyDescriptorCollection that represents the | |
// filtered properties for this component instance. | |
PropertyDescriptorCollection GetProperties(Attribute[] attributes); |
This is also more to do with WPF Binding System. If the DataContext implements ICustomTypeDescriptor then WPF Binding system uses the Type Descriptor instead of relying on reflection. From msdn, this discussion is very useful to learn how binding references are resolved.
http://msdn.microsoft.com/en-us/library/bb613546.aspx
Both of the overloads of GetProperties() methods return PropertyDescriptorCollection. It is a collection of PropertyDescriptor. Overriding these methods and adding further items to the collection can give the impression to the WPF Binding system of having these properties by the type itself. Let's provide a descriptor inheriting from PropertyDescriptor.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class CustomPropertyDescriptor<T> : PropertyDescriptor | |
{ | |
private Type propertyType; | |
private Type componentType; | |
T propertyValue; | |
public CustomPropertyDescriptor(string propertyName, Type componentType) | |
: base(propertyName, new Attribute[] { }) | |
{ | |
this.propertyType = typeof(T); | |
this.componentType = componentType; | |
} | |
public override bool CanResetValue(object component) { return true; } | |
public override Type ComponentType { get { return componentType; } } | |
public override object GetValue(object component) | |
{ | |
return propertyValue; | |
} | |
public override bool IsReadOnly { get { return false; } } | |
public override Type PropertyType { get { return propertyType; } } | |
public override void ResetValue(object component) { SetValue(component, default(T)); } | |
public override void SetValue(object component, object value) | |
{ | |
if (!value.GetType().IsAssignableFrom(propertyType)) | |
{ | |
throw new System.Exception("Invalid type to assign"); | |
} | |
propertyValue = (T)value; | |
} | |
public override bool ShouldSerializeValue(object component) { return true; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
internal class ViewModelBase : CustomTypeDescriptor, INotifyPropertyChanged | |
{ | |
#region Private Fields | |
List<PropertyDescriptor> _myProperties = new List<PropertyDescriptor>(); | |
#endregion | |
#region INotifyPropertyChange Implementation | |
public event PropertyChangedEventHandler PropertyChanged = delegate { }; | |
protected void OnPropertyChanged(string propertyName) | |
{ | |
PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); | |
} | |
#endregion INotifyPropertyChange Implementation | |
#region Public Methods | |
public void SetPropertyValue<T>(string propertyName, T propertyValue) | |
{ | |
var properties = this.GetProperties() | |
.Cast<PropertyDescriptor>() | |
.Where(prop => prop.Name.Equals(propertyName)); | |
if (properties == null || properties.Count() != 1) | |
{ | |
throw new Exception("The property doesn't exist."); | |
} | |
var property = properties.First(); | |
property.SetValue(this, propertyValue); | |
OnPropertyChanged(propertyName); | |
} | |
public T GetPropertyValue<T>(string propertyName) | |
{ | |
var properties = this.GetProperties() | |
.Cast<PropertyDescriptor>() | |
.Where(prop => prop.Name.Equals(propertyName)); | |
if (properties == null || properties.Count() != 1) | |
{ | |
throw new Exception("The property doesn't exist."); | |
} | |
var property = properties.First(); | |
return (T)property.GetValue(this); | |
} | |
public void AddProperty<T, U>(string propertyName) where U : ViewModelBase | |
{ | |
var customProperty = | |
new CustomPropertyDescriptor<T>( | |
propertyName, | |
typeof(U)); | |
_myProperties.Add(customProperty); | |
} | |
#endregion | |
#region Overriden Methods | |
public override PropertyDescriptorCollection GetProperties() | |
{ | |
var properties = base.GetProperties(); | |
return new PropertyDescriptorCollection( | |
properties.Cast<PropertyDescriptor>() | |
.Concat(_myProperties).ToArray()); | |
} | |
#endregion | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
internal class StudentViewModel : ViewModelBase | |
{ | |
#region Constructors | |
public StudentViewModel() | |
{ | |
AddProperty<string, StudentViewModel>("LastName"); | |
} | |
#endregion | |
#region Properties | |
private string _firstName; | |
public string FirstName | |
{ | |
get { return _firstName; } | |
set | |
{ | |
_firstName = value; | |
OnPropertyChanged("FirstName"); | |
} | |
} | |
#endregion | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
internal class MainViewModel : ViewModelBase | |
{ | |
#region Constructors | |
public MainViewModel() | |
{ | |
AddProperty<string, MainViewModel>("MainTitle"); | |
SetPropertyValue<string>("MainTitle", "Main App Window"); | |
} | |
#endregion | |
#region Properties | |
private ObservableCollection<StudentViewModel> _students; | |
public ObservableCollection<StudentViewModel> Students | |
{ | |
get | |
{ | |
if (_students == null) | |
{ | |
_students = new ObservableCollection<StudentViewModel>(); | |
var student1 = new StudentViewModel { FirstName = "Muhammad" }; | |
student1.SetPropertyValue<string>("LastName", "Siddiqi"); | |
_students.Add(student1); | |
var student2 = new StudentViewModel { FirstName = "Koi" }; | |
student2.SetPropertyValue<string>("LastName", "Aur"); | |
_students.Add(student2); | |
} | |
return _students; | |
} | |
} | |
private StudentViewModel _selectedStudent; | |
public StudentViewModel SelectedStudent | |
{ | |
get { return _selectedStudent; } | |
set | |
{ | |
_selectedStudent = new StudentViewModel(); | |
OnPropertyChanged("SelectedStudent"); | |
} | |
} | |
#endregion | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Window x:Class="WPFBinding_CustomTypeDescriptor.MainWindow" | |
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | |
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | |
xmlns:local="clr-namespace:WPFBinding_CustomTypeDescriptor" | |
Title="MainWindow" Height="350" Width="525"> | |
<Window.DataContext> | |
<local:MainViewModel /> | |
</Window.DataContext> | |
<Grid> | |
<Grid.RowDefinitions> | |
<RowDefinition Height="auto" /> | |
<RowDefinition Height="auto" /> | |
<RowDefinition Height="auto" /> | |
<RowDefinition Height="auto" /> | |
<RowDefinition Height="*" /> | |
</Grid.RowDefinitions> | |
<TextBox Text="{Binding MainTitle, UpdateSourceTrigger=PropertyChanged}" | |
Grid.Row="0" | |
Background="GreenYellow" | |
FontWeight="Bold" FontSize="24" TextAlignment="Center" /> | |
<TextBlock Text="Students ListBox" Grid.Row="1" Margin="10,10,0,10" | |
FontSize="18" FontStyle="Italic"/> | |
<ListBox ItemsSource="{Binding Students}" Grid.Row="2"> | |
<ListBox.ItemTemplate> | |
<DataTemplate> | |
<TextBlock> | |
<TextBlock.Text> | |
<MultiBinding StringFormat="{}{0} {1}"> | |
<Binding Path="FirstName" Mode="OneWay"/> | |
<Binding Path="LastName" Mode="OneWay"/> | |
</MultiBinding> | |
</TextBlock.Text> | |
</TextBlock> | |
</DataTemplate> | |
</ListBox.ItemTemplate> | |
</ListBox> | |
<TextBlock Text="Students DataGrid" Grid.Row="3" Margin="10,10,0,10" | |
FontSize="18" FontStyle="Italic"/> | |
<DataGrid AutoGenerateColumns="False" IsEnabled="True" | |
x:Name="studentsGrid" | |
ItemsSource="{Binding Students}" | |
Grid.Row="4" Margin="0,16,0,-16"> | |
<DataGrid.Columns> | |
<DataGridTextColumn Header="First Name" | |
Binding="{Binding FirstName, Mode=OneWay}"/> | |
<DataGridTextColumn Header="Last Name" | |
Binding="{Binding LastName, Mode=OneWay}" /> | |
</DataGrid.Columns> | |
</DataGrid> | |
</Grid> | |
</Window> |
Supporting Change Notifications
This looks perfect but this has one limitation. This doesn't support change notifications for newly added properties. Since we are using PropertyDescritptor to add dynamic properties, we can easily add this behavior using OnValueChanged from the type. We first need to update the ViewModelBase so that whenever a property is added, we add the ValueChanged to the property descriptor and call OnPropertyChanged from ViewModelBase similary to the existing properties from the inheriting types.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public void AddProperty<T, U>(string propertyName) where U : ViewModelBase | |
{ | |
var customProperty = | |
new CustomPropertyDescriptor<T>( | |
propertyName, | |
typeof(U)); | |
_myProperties.Add(customProperty); | |
customProperty.AddValueChanged( | |
this, | |
(o, e) => { OnPropertyChanged(propertyName); }); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public override void SetValue(object component, object value) | |
{ | |
if (!value.GetType().IsAssignableFrom(propertyType)) | |
{ | |
throw new System.Exception("Invalid type to assign"); | |
} | |
propertyValue = (T)value; | |
OnValueChanged(component, new EventArgs()); | |
} |
Download
5 comments:
Muhammad, thanks for the provided solution. It's amazing.
Now I try to apply it to my WPF PropertyGrid.
Right now, all Students share the same properties. Could you show how to implement properties that are specific for each model object. E.g. the first Student (Muhammad) has different number of properties than the second (Koi).
Hi Koi,
Glad to help! Basically this solution is just for that purpose when properties are defined on type level. For instance specific properties, I would rather add a key / value pair collection to the type.
The thing is that I have mixed properties: type specific and instance specific. How could I merge a key-value Dictionary of with your solution. I found a good way to bind a Dictionary: http://stackoverflow.com/questions/3669660/can-propertygrid-edit-any-old-list-of-key-value-pairs
But as I said, it's a total different approach to yours and I don't know how to merge it. I like your linq style, so I would prefer yours as base ;-)
Hi,
You have in response to Koi's question mentioned that this particular solution is to define the properties at type level and not instance level. But I think it is not the case.
In your example, you need to make a call for "AddProperty" either on each instance object or somewhere in the class definition in order to add particular property to that instance object. Both these defeats the purpose of adding properties dynamically.
Ideally, "AddProperty" method should be static property so that no need to make "AddProperty" call on each instance object in order to add the dynamic property. Do you see the possibility of making "AddProperty" static?
Hello, you said that it also works for dynamic properties, i thought you meant DynamicObject, but our class already inherited from your base class, how to make it truly dynamic?
I see 2 ways:
1) Implement Methods of ICustomTypeDescriptor directly in class (but here i have some problems , for example i dont know hwere _parent might come from and how to use it correctly)
2) Use AddProperty method to add properties on runtime with values. But when i do that somehow all my values shown the same for all objects in collection.
Do you have any thoughts on that? Thank you
Post a Comment