In one of my projects I had to create something like Visual Studio property window, for data set of different controls/data classes. Those controls/classes are still under development and I needed the way to display/change values of those properties dynamically without even knowing what is inside.
For the simple case, let’s assume the following class which holds the data:
public class SampleData
{
public SampleData()
{
BooleanProperty = false;
}
public string StringProperty { get; set; }
public bool BooleanProperty { get; set; }
public Button ButtonProperty { get; set; }
public static double StaticDoubleFiled = 123;
public static string StaticStringFiled = "This is static field";
public int IntFiled = 345;
public string StringFiled = "This is non static field";
public float FloatProperty = 456.0f;
}
The class fields/properties should be displayed dynamically. I decided to create an attribute to decorate the relevant fields/properties and then get the values/metadata from attributes on-the-fly via reflection.
So here is a sample attribute I’ve create for this:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
internal sealed class PropertyFieldDescriptionAttribute : Attribute
{
#region .ctor
public PropertyFieldDescriptionAttribute(string DisplayName)
{
this.displayName = DisplayName;
this.IsReadOnly = false;
}
#endregion
#region Public proerties
public string DisplayName
{
get { return displayName; }
}
public string Description { get; set; }
public string DefaultValue { get; set; }
public bool IsReadOnly { get; set; }
#endregion
#region Private variables
readonly string displayName;
#endregion
}
And decorated fields/properties with the attribute (partial code):
[PropertyFieldDescription("Sample String Property", Description="This is sample string property")]
public string StringProperty { get; set; }
[PropertyFieldDescription("Sample Boolean Property", Description = "This is sample boolean property", DefaultValue="false")]
public bool BooleanProperty { get; set; }
[PropertyFieldDescription("Sample Button Property", Description = "This is sample button property")]
public Button ButtonProperty { get; set; }
From here everything were pretty straight forward – sample code for getting fields information:
FieldInfo[] fields = type.GetFields(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public);
if (fields.Length > 0)
{
foreach (var field in fields)
{
PropertyFieldDescriptionAttribute[] attribs = (PropertyFieldDescriptionAttribute[])field.GetCustomAttributes(
typeof(PropertyFieldDescriptionAttribute), false);
if (attribs.Length == 1) //Each valid property in this sample case should have only 1(!) such attribute
{
//Define data class to hold it
FieldsPropertiesData constant = new FieldsPropertiesData(field.Name, field.FieldType);
if (field.IsStatic)
constant.FiledValue = field.GetValue(null);
else
constant.FiledValue = field.GetValue(instance);
constant.DefaultValue = attribs[0].DefaultValue;
constant.Description = attribs[0].Description;
constant.DisplayName = attribs[0].DisplayName;
constant.ReadOnly = attribs[0].IsReadOnly;
//add to collection, which will be databoud to the screen
constants.Add(constant);
}
}
}
and almost the same function for the properties.
The results I stored in “ObservableCollection<FieldsPropertiesData>” for easy data binding.
The FieldsPropertiesData of mine is like follows:
public class FieldsPropertiesData
{
#region .ctor
public FieldsPropertiesData(string FiledName, Type FieldType)
{
this.FieldName = FieldName;
this.FieldType = FieldType;
}
#endregion
#region Public properties
public object FiledValue { get; set; }
public Type FieldType { get; private set; }
public string FieldName { get; private set; }
public string DisplayName { get; set; }
public string Description { get; set; }
public string DefaultValue { get; set; }
public bool ReadOnly { get; set; }
#endregion
}
I’ve created 2 simple custom controls – one to serve as a grid for my properties grid, and another to hold data item
The Grid:
[TemplatePart(Name="lst", Type=typeof(ListBox))]
public class FieldsPropertiesGrid : Control
{
public FieldsPropertiesGrid()
{
this.DefaultStyleKey = typeof(FieldsPropertiesGrid);
}
public ObservableCollection<FieldsPropertiesData> Data
{
get { return (ObservableCollection<FieldsPropertiesData>)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
// Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(ObservableCollection<FieldsPropertiesData>), typeof(FieldsPropertiesGrid), null);
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
ListBox lst = GetTemplateChild("lst") as ListBox;
lst.DataContext = this;
}
}
The DataItem (partial):
public class FiledPropertyData : Control
{
public FiledPropertyData()
{
this.DefaultStyleKey = typeof(FiledPropertyData);
}
public bool IsReadOnly
{
get { return (bool)GetValue(IsReadOnlyProperty); }
set { SetValue(IsReadOnlyProperty, value); }
}
// Using a DependencyProperty as the backing store for IsReadOnly. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsReadOnlyProperty =
DependencyProperty.Register("IsReadOnly", typeof(bool), typeof(FiledPropertyData), null);
public string DefaultValue
{
get { return (string)GetValue(DefaultValueProperty); }
set { SetValue(DefaultValueProperty, value); }
}
// And following depenedency properties: DefaultValue, Description, DisplayName, etc.
// ...
}
The default template for the controls:
<Style TargetType="local:FieldsPropertiesGrid">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:FieldsPropertiesGrid">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ListBox x:Name="lst" ItemsSource="{Binding Data}">
<ListBox.ItemTemplate>
<DataTemplate>
<local:FiledPropertyData Margin="0,2.5"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="local:FiledPropertyData">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:FiledPropertyData">
<Grid ToolTipService.Placement="Mouse" Width="{TemplateBinding Width}">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding DisplayName}" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="0,0,10,0"/>
<TextBox Text="{Binding DefaultValue}" VerticalAlignment="Center" HorizontalAlignment="Right" IsEnabled="{Binding IsReadOnly, Converter={StaticResource boolToEnabledConverter}}" Grid.Column="1"/>
<ToolTipService.ToolTip>
<ToolTip Background="Transparent" BorderBrush="Transparent" BorderThickness="0" Padding="0" Margin="5">
<ToolTip.Content>
<Border Background="PaleGoldenrod" BorderBrush="Black" BorderThickness="2" CornerRadius="5" Padding="5" Margin="0">
<TextBlock Text="{Binding Description}"/>
</Border>
</ToolTip.Content>
</ToolTip>
</ToolTipService.ToolTip>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The sample page creates an instance of SampleData class, and displays attributed the properties/fields in the fields grid:
<Grid x:Name="LayoutRoot" Background="White">
<Grid.Resources>
<local:PropetiesConverter x:Key="propertiesConverter"/>
<Button x:Key="theSampleButton" Content="Sample Button" Width="100" Height="25"/>
<local:SampleData x:Key="theSampleData" BooleanProperty="True" StringProperty="Test String"
ButtonProperty="{StaticResource theSampleButton}"/>
</Grid.Resources>
<local:FieldsPropertiesGrid x:Name="propGrid" Data="{Binding Source={StaticResource theSampleData},
Converter={StaticResource propertiesConverter}}"/>
</Grid>
The last thing here is the converter which get’s the instance and returns the ObservableCollection with properties/fields info:
public class PropetiesConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
Type destType = value.GetType();
ObservableCollection<FieldsPropertiesData> props = ReflectionHelper<SampleData>.GetProperties((SampleData)value);
return props;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
That’s it – not any type with known attributes will be displayed in the Properties Grid:
If adding new Field/Property with the attribute it will be shown next time the Properties grid displayed:
[PropertyFieldDescription("Additional String Field", Description = "This is additional sample string field",
DefaultValue = "Test Value")]
public string AdditionaStringProperty = "New Value";
The source code here.
Running sample here.
Enjoy,
Alex