Table of Contents
Introduction
When we want to test our .NET applications, in addition to the entire application test, we sometimes want to give a way to test some of our algorithms individually.
Usually, for achieving this goal, I used to create some GUI screens that enable entering the needed parameters and, invoking the wanted methods using them.
In that way, frequently, I found myself wasting a lot of time only for developing GUI screens that aren't a part of the developed application. Beside the first development of the screens, every time that a method's signature had been changed, I had to change those GUI screens too. In that way, every module that I had to test, gave me another screen to maintain.
Since I got tired of doing that any time I needed to test a class, I decided to write a control that simplifies this task.
Background
In this solution, we create a WPF control that can present the needed GUI for every class that we want to test and, is updated according to the changes of that class.
For presenting values, we create view-models and, appropriate DataTemplate
for presenting them.
For more information about it, you can read the MSDN topics about Reflection, Control Authoring, Routed Events, Commands and, the MVVM pattern.
How It Works
Handling Values
Value view-model
Before creating a control that enables methods' invoke, we need a way to get the values of the methods' parameters. for that purpose, we create a view-model that presents the value's details:
public class BaseViewModel : INotifyPropertyChanged
{
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string propName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propName));
}
}
#endregion
}
class ValueViewModel : BaseViewModel
{
}
Add properties for the value's name, values' type and, the value's value:
#region Name
private string _name;
public string Name
{
get { return _name; }
protected set
{
if (_name != value)
{
_name = value;
NotifyPropertyChanged("Name");
NotifyPropertyChanged("HasName");
}
}
}
#endregion
#region HasName
public bool HasName
{
get { return !string.IsNullOrEmpty(Name); }
}
#endregion
#region ValueType
private Type _valueType;
public Type ValueType
{
get { return _valueType; }
set
{
if (_valueType != value)
{
Type oldValue = _valueType;
_valueType = value;
OnValueTypeChanged(oldValue, _valueType);
}
}
}
protected virtual void OnValueTypeChanged(Type oldValue, Type newValue)
{
NotifyPropertyChanged("ValueType");
}
#endregion
#region Value
protected object _value;
public virtual object Value
{
get { return _value; }
set
{
if (_value != value)
{
object oldValue = _value;
_value = value;
OnValueChanged(oldValue, _value);
}
}
}
protected virtual void OnValueChanged(object oldValue, object newValue)
{
NotifyPropertyChanged("Value");
}
#endregion
And, add properties for indicating the value's kind and content:
#region IsCollection
public bool IsCollection
{
get
{
Type t = SelectedCompatibleType;
if (t == null)
{
return false;
}
if (t == typeof(string))
{
return false;
}
Type[] typeIntefaces = t.GetInterfaces();
if (typeIntefaces.Contains(typeof(IEnumerable)))
{
return true;
}
return false;
}
}
#endregion
#region IsString
public bool IsString
{
get { return SelectedCompatibleType == typeof(string); }
}
#endregion
#region IsParsable
public bool IsParsable
{
get
{
Type t = SelectedCompatibleType;
if (t == null)
{
return false;
}
MethodInfo parseMethod = t.GetMethod("Parse", new[] { typeof(string) });
if (parseMethod != null && parseMethod.IsStatic)
{
return true;
}
return false;
}
}
#endregion
#region IsEnum
public bool IsEnum
{
get
{
Type t = SelectedCompatibleType;
if (t == null)
{
return false;
}
return t.IsEnum;
}
}
#endregion
#region IsBoolean
public bool IsBoolean
{
get { return SelectedCompatibleType == typeof(bool); }
}
#endregion
Handling Derived Types
In order to support getting values of types that derive from the parameters' values' types, we have to find the whole of the types that compatible to the parameter's type. That can be done as the following:
#region KnownTypes
private IEnumerable<Type> _knownTypes;
public IEnumerable<Type> KnownTypes
{
get { return _knownTypes; }
set
{
if (_knownTypes != value)
{
IEnumerable<Type> oldValue = _knownTypes;
_knownTypes = value;
OnKnownTypesChanged(oldValue, _knownTypes);
}
}
}
protected virtual void OnKnownTypesChanged
(IEnumerable<Type> oldValue, IEnumerable<Type> newValue)
{
NotifyPropertyChanged("KnownTypes");
}
#endregion
#region AutoGenerateCompatibleTypes
private bool _autoGenerateCompatibleTypes;
public bool AutoGenerateCompatibleTypes
{
get { return _autoGenerateCompatibleTypes; }
set
{
if (_autoGenerateCompatibleTypes != value)
{
bool oldValue = _autoGenerateCompatibleTypes;
_autoGenerateCompatibleTypes = value;
OnAutoGenerateCompatibleTypesChanged(oldValue, _autoGenerateCompatibleTypes);
}
}
}
protected virtual void OnAutoGenerateCompatibleTypesChanged(bool oldValue, bool newValue)
{
NotifyPropertyChanged("AutoGenerateCompatibleTypes");
}
#endregion
private List<Type> GetCompatibleTypes(Type baseType)
{
List<Type> res = new List<Type>();
if (!baseType.IsAbstract)
{
res.Add(baseType);
}
AddKnownCompatibleTypes(baseType, res);
if (AutoGenerateCompatibleTypes &&
baseType != typeof(object))
{
AddCompatibleTypesFromLoadedAssemblies(baseType, res);
}
return res.Distinct().ToList();
}
private void AddKnownCompatibleTypes(Type baseType, List<Type> res)
{
if (KnownTypes != null)
{
foreach (Type t in KnownTypes)
{
if (t.IsSubclassOf(baseType) && !t.IsAbstract)
{
res.Add(t);
}
}
}
}
private static void AddCompatibleTypesFromLoadedAssemblies(Type baseType, List<Type> res)
{
Assembly[] assemblies = GetLoadedAssemblies();
if (assemblies == null)
{
return;
}
foreach (Assembly a in assemblies)
{
if (a == null)
{
continue;
}
foreach (Type t in a.GetTypes())
{
if (t.IsSubclassOf(baseType) && !t.IsAbstract)
{
res.Add(t);
}
}
}
}
static private Assembly[] GetLoadedAssemblies()
{
AppDomain currDomain = AppDomain.CurrentDomain;
if (currDomain == null)
{
return null;
}
return currDomain.GetAssemblies();
}
The GetCompatibleTypes
method generates a list with the parameter's type (if it isn't abstract) and, types in the loaded assemblies that derive from the parameter's type. There is also an option to generate types only from a given types list, using the KnownTypes
and the AutoGenerateCompatibleTypes
properties appropriately.
We generate the compatible types and set the default selected compatible type, for each time the ValueType
is changed, as the following:
#region CompatibleTypes
private ObservableCollection<Type> _compatibleTypes;
public ObservableCollection<Type> CompatibleTypes
{
get { return _compatibleTypes ?? (_compatibleTypes = new ObservableCollection<Type>()); }
}
#endregion
#region SelectedCompatibleType
private Type _selectedCompatibleType;
public Type SelectedCompatibleType
{
get { return _selectedCompatibleType; }
set
{
if (_selectedCompatibleType != value)
{
Type oldValue = _selectedCompatibleType;
_selectedCompatibleType = value;
OnSelectedCompatibleTypeChanged(oldValue, _selectedCompatibleType);
}
}
}
protected virtual void OnSelectedCompatibleTypeChanged(Type oldValue, Type newValue)
{
NotifyPropertyChanged("IsString");
NotifyPropertyChanged("IsParsable");
NotifyPropertyChanged("IsCollection");
NotifyPropertyChanged("IsEnum");
NotifyPropertyChanged("IsBoolean");
NotifyPropertyChanged("Value");
}
#endregion
protected virtual void OnValueTypeChanged(Type oldValue, Type newValue)
{
UpdateCompatibleTypes();
NotifyPropertyChanged("ValueType");
}
protected void UpdateCompatibleTypes()
{
CompatibleTypes.Clear();
if (ValueType == null)
{
return;
}
GetCompatibleTypes(ValueType).ForEach(t => CompatibleTypes.Add(t));
SelectedCompatibleType = CompatibleTypes.FirstOrDefault();
}
Handling Complex Types
In order to support getting values of complex types, we have to present the value of the properties and the fields of the type. That can be done as the following:
#region SubFields
private ObservableCollection<ValueViewModel> _subFields;
public ObservableCollection<ValueViewModel> SubFields
{
get { return _subFields ?? (_subFields = new ObservableCollection<ValueViewModel>()); }
}
#endregion
#region HasSubFields
public bool HasSubFields
{
get { return SubFields.Count != 0; }
}
#endregion
protected virtual void GenerateSubFieldsIfNeeded()
{
SubFields.Clear();
Type t = SelectedCompatibleType;
if (t == null)
{
return;
}
if (IsString ||
IsParsable ||
IsCollection ||
IsEnum)
{
return;
}
PropertyInfo[] typeProperties = t.GetProperties();
foreach (PropertyInfo pi in typeProperties)
{
if (pi.CanWrite)
{
AddPropertyValueViewModel(pi);
}
}
FieldInfo[] typeFields = t.GetFields();
foreach (FieldInfo fi in typeFields)
{
if (fi.IsPublic)
{
AddFieldValueViewModel(fi);
}
}
}
protected abstract void AddPropertyValueViewModel(PropertyInfo pi);
protected abstract void AddFieldValueViewModel(FieldInfo fi);
The GenerateSubFieldsIfNeeded
method calls the AddPropertyValueViewModel
abstract
method for the properties of the type and, calls the AddFieldValueViewModel
abstract
method for the fields of the type. These methods are implemented in the derived classes appropriately.
For getting values, we derive ValueViewModel
as the following:
public class InputValueViewModel : ValueViewModel
{
public InputValueViewModel()
{
IsEditable = true;
}
protected override void AddPropertyValueViewModel(PropertyInfo pi)
{
PropertyInputValueViewModel subField = new PropertyInputValueViewModel(pi)
{
KnownTypes = KnownTypes,
AutoGenerateCompatibleTypes = AutoGenerateCompatibleTypes
};
SubFields.Add(subField);
}
protected override void AddFieldValueViewModel(FieldInfo fi)
{
FieldInputValueViewModel subField = new FieldInputValueViewModel(fi)
{
KnownTypes = KnownTypes,
AutoGenerateCompatibleTypes = AutoGenerateCompatibleTypes
};
SubFields.Add(subField);
}
}
public class PropertyInputValueViewModel : InputValueViewModel
{
public PropertyInputValueViewModel(PropertyInfo pi)
{
PropertyInformation = pi;
}
#region PropertyInformation
private PropertyInfo _propertyInformation;
public PropertyInfo PropertyInformation
{
protected get { return _propertyInformation; }
set
{
if (_propertyInformation != value)
{
PropertyInfo oldValue = _propertyInformation;
_propertyInformation = value;
OnPropertyInformationChanged(oldValue, _propertyInformation);
}
}
}
protected virtual void OnPropertyInformationChanged
(PropertyInfo oldValue, PropertyInfo newValue)
{
if (newValue == null)
{
return;
}
Name = newValue.Name;
ValueType = newValue.PropertyType;
}
#endregion
}
public class FieldInputValueViewModel : InputValueViewModel
{
public FieldInputValueViewModel(FieldInfo fi)
{
FieldInformation = fi;
}
#region FieldInformation
private FieldInfo _fieldInformation;
public FieldInfo FieldInformation
{
protected get { return _fieldInformation; }
set
{
if (_fieldInformation != value)
{
FieldInfo oldValue = _fieldInformation;
_fieldInformation = value;
OnFieldInformationChanged(oldValue, _fieldInformation);
}
}
}
protected virtual void OnFieldInformationChanged
(FieldInfo oldValue, FieldInfo newValue)
{
if (newValue == null)
{
return;
}
Name = newValue.Name;
ValueType = newValue.FieldType;
}
#endregion
}
The IsEditable
property is defined in ValueViewModel
as the following:
#region IsEditable
private bool _isEditable;
public bool IsEditable
{
get { return _isEditable; }
protected set
{
if (_isEditable != value)
{
_isEditable = value;
NotifyPropertyChanged("IsEditable");
}
}
}
#endregion
For presenting values, we derive ValueViewModel
as the following:
public class OutputValueViewModel : ValueViewModel
{
#region Constructors
public OutputValueViewModel(object value)
{
IsEditable = false;
if (value != null)
{
Type valueType = value.GetType();
if (valueType.IsEnum)
{
Value = Enum.GetName(valueType, value);
ValueType = typeof(string);
}
else if (value is Exception)
{
Value = new ExceptionData(value as Exception);
ValueType = typeof(ExceptionData);
}
else
{
Value = value;
ValueType = value.GetType();
}
}
}
#endregion
protected override void AddFieldValueViewModel(FieldInfo fi)
{
if (fi == null || Value == null)
{
return;
}
object fieldValue = fi.GetValue(Value);
SubFields.Add(new OutputValueViewModel(fieldValue)
{
Name = fi.Name
});
}
protected override void AddPropertyValueViewModel(PropertyInfo pi)
{
if (pi == null || Value == null)
{
return;
}
object prorertyValue = pi.GetValue(Value, null);
SubFields.Add(new OutputValueViewModel(prorertyValue)
{
Name = pi.Name
});
}
}
The OutputValueViewModel
class uses the ExceptionData
class for presenting exceptions. This class is implemented as the following:
public class ExceptionData
{
public ExceptionData(Exception ex)
{
if (ex != null)
{
ExceptionType = ex.GetType().FullName;
ExceptionMessage = ex.Message;
if (ex.InnerException != null)
{
InnerException = new ExceptionData(ex.InnerException);
}
}
}
public string ExceptionType { get; set; }
public string ExceptionMessage { get; set; }
public ExceptionData InnerException { get; set; }
}
Handling Collections
In order to support getting collections, we have to let the user to add and remove collections' elements. We can do that by adding commands for adding and removing collections' elements:
#region CollectionElements
private ObservableCollection<ValueViewModel> _collectionElements;
public ObservableCollection<ValueViewModel> CollectionElements
{
get { return _collectionElements ??
(_collectionElements = new ObservableCollection<ValueViewModel>()); }
}
#endregion
#region IsRemovable
private bool _isRemovable;
public bool IsRemovable
{
get { return _isRemovable; }
protected set
{
if (_isRemovable != value)
{
_isRemovable = value;
NotifyPropertyChanged("IsRemovable");
}
}
}
#endregion
#region AddNewCollectionElementCommand
private ICommand _addNewCollectionElementCommand;
public ICommand AddNewCollectionElementCommand
{
get
{
if (_addNewCollectionElementCommand == null)
{
_addNewCollectionElementCommand =
new GeneralCommand(o => AddNewCollectionElement(true), o => IsCollection);
}
return _addNewCollectionElementCommand;
}
}
protected void AddNewCollectionElement(bool notifyValueChanged)
{
InputValueViewModel element = new InputValueViewModel
{
ValueType = CollectionElementType,
IsRemovable = true,
Removed = OnCollectionElementRemoved,
KnownTypes = KnownTypes,
AutoGenerateCompatibleTypes = AutoGenerateCompatibleTypes
};
CollectionElements.Add(element);
if (notifyValueChanged)
{
NotifyPropertyChanged("Value");
}
}
protected void OnCollectionElementRemoved(ValueViewModel element)
{
CollectionElements.Remove(element);
}
#endregion
#region RemoveCommand
private ICommand _removeCommand;
public ICommand RemoveCommand
{
get { return _removeCommand ??
(_removeCommand = new GeneralCommand(o => Remove(), o => IsRemovable)); }
}
protected void Remove()
{
Action<ValueViewModel> handler = Removed;
if (handler != null)
{
handler(this);
}
}
public Action<ValueViewModel> Removed;
#endregion
For creating the commands, we use the GeneralCommand
class. This class is implemented as the following:
public class GeneralCommand : ICommand
{
private Action<object> _execute;
private Predicate<object> _canExecute;
public GeneralCommand(Action<object> execute, Predicate<object> canExecute)
{
_execute = execute;
_canExecute = canExecute;
}
#region ICommand Members
public bool CanExecute(object parameter)
{
if (_canExecute != null)
{
return _canExecute(parameter);
}
return true;
}
public event EventHandler CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
}
remove
{
CommandManager.RequerySuggested -= value;
}
}
public void Execute(object parameter)
{
if (_execute != null)
{
_execute(parameter);
}
}
#endregion
}
For presenting collections, we override the GenerateSubFieldsIfNeeded
method in OutputValueViewModel
as the following:
protected override void GenerateSubFieldsIfNeeded()
{
base.GenerateSubFieldsIfNeeded();
if (IsCollection)
{
IEnumerable ie = Value as IEnumerable;
if (ie != null)
{
foreach (object val in ie)
{
CollectionElements.Add(new OutputValueViewModel(val));
}
}
}
}
Handling Null Values
In order to support getting null
values, we add properties to indicate that the value is null
:
#region IsNullable
public bool IsNullable
{
get
{
Type t = SelectedCompatibleType;
return t != null ? !t.IsValueType : false;
}
}
#endregion
#region IsNull
protected bool _isNull;
public bool IsNull
{
get { return _isNull; }
set
{
if (_isNull != value)
{
bool oldValue = _isNull;
_isNull = value;
OnIsNullChanged(oldValue, _isNull);
NotifyPropertyChanged("IsNull");
}
}
}
protected virtual void OnIsNullChanged(bool oldValue, bool newValue)
{
}
#endregion
Value data-template
After we have the view-model, we create a DataTemplate
for presenting this view-model:
<DataTemplate DataType="{x:Type local:ValueViewModel}">
</DataTemplate>
In this DataTemplate
, we put a Grid
and, separate it to 3 columns:
<DataTemplate DataType="{x:Type local:ValueViewModel}">
<Grid x:Name="rootElement"
HorizontalAlignment="Stretch"
Margin="1">
<Grid.LayoutTransform>
<ScaleTransform ScaleX="0" ScaleY="0" />
</Grid.LayoutTransform>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
</Grid>
</DataTemplate>
In this Grid
, we add a Remove button:
<DataTemplate DataType="{x:Type local:ValueViewModel}">
<DataTemplate.Resources>
<Storyboard x:Key="hideRootElementStoryboard">
<DoubleAnimation Storyboard.TargetName="rootElement"
Storyboard.TargetProperty="(FrameworkElement.
LayoutTransform).(ScaleTransform.ScaleX)"
To="0"
Duration="0:0:0.2" />
<DoubleAnimation Storyboard.TargetName="rootElement"
Storyboard.TargetProperty="(FrameworkElement.
LayoutTransform).(ScaleTransform.ScaleY)"
To="0"
Duration="0:0:0.2" />
</Storyboard>
</DataTemplate.Resources>
<Grid x:Name="rootElement"
HorizontalAlignment="Stretch"
Margin="1">
...
<local:SuspendedButton x:Name="btnRemove"
Style="{StaticResource removeButtonStyle}"
ToolTip="Remove element."
SuspendTime="0:0:0.2"
Margin="1"
Command="{Binding RemoveCommand}"
Visibility="Collapsed"
VerticalAlignment="Top"
HorizontalAlignment="Left" />
</Grid>
<DataTemplate.Triggers>
<EventTrigger RoutedEvent="local:SuspendedButton.BeforeClick"
SourceName="btnRemove">
<BeginStoryboard Storyboard=
"{StaticResource hideRootElementStoryboard}" />
</EventTrigger>
</DataTemplate.Triggers>
</DataTemplate>
Add a region for the field's name and value:
<Grid x:Name="rootElement"
HorizontalAlignment="Stretch"
Margin="1">
...
<Grid x:Name="fieldRegion"
Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal"
Visibility="{Binding HasName,
Converter={StaticResource BooleanToVisibilityConverter}}">
<TextBlock Text="{Binding Name}" />
<TextBlock Text=": " />
</StackPanel>
<Grid x:Name="isNullRegion"
Grid.Column="1"
Margin="2">
<ToggleButton Visibility="{Binding IsNullable,
Converter={StaticResource BooleanToVisibilityConverter}}"
Style="{StaticResource nullButtonStyle}"
VerticalAlignment="Top"
IsChecked="{Binding IsNull, Mode=TwoWay}" />
</Grid>
<ContentControl x:Name="fieldValue"
Content="{Binding}"
Grid.Column="2" />
</Grid>
</Grid>
and, add a region for additional compatible types:
<DataTemplate DataType="{x:Type local:ValueViewModel}">
<DataTemplate.Resources>
...
<Storyboard x:Key="showTypesStoryboard">
<DoubleAnimation Storyboard.TargetName="typesBorder"
Storyboard.TargetProperty=
"(FrameworkElement.LayoutTransform).(ScaleTransform.ScaleX)"
To="1"
Duration="0:0:0.2" />
<DoubleAnimation Storyboard.TargetName="typesBorder"
Storyboard.TargetProperty=
"(FrameworkElement.LayoutTransform).(ScaleTransform.ScaleY)"
To="1"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Key="hideTypesStoryboard">
<DoubleAnimation Storyboard.TargetName="typesBorder"
Storyboard.TargetProperty=
"(FrameworkElement.LayoutTransform).(ScaleTransform.ScaleX)"
To="0"
Duration="0:0:0.2" />
<DoubleAnimation Storyboard.TargetName="typesBorder"
Storyboard.TargetProperty=
"(FrameworkElement.LayoutTransform).(ScaleTransform.ScaleY)"
To="0"
Duration="0:0:0.2" />
</Storyboard>
</DataTemplate.Resources>
<Grid x:Name="rootElement"
HorizontalAlignment="Stretch"
Margin="1">
...
<Grid Visibility="{Binding HasAdditionalCompatibleTypes,
Converter={StaticResource BooleanToVisibilityConverter}}"
Grid.Column="2"
VerticalAlignment="Top"
Margin="5,0,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<ToggleButton x:Name="typesToggle"
Style="{StaticResource expandButtonStyle}"
HorizontalAlignment="Right"
ToolTip="Expand the compatible types region.">
<ToggleButton.LayoutTransform>
<ScaleTransform ScaleX="-1" />
</ToggleButton.LayoutTransform>
</ToggleButton>
<Border x:Name="typesBorder"
Grid.Row="1"
Margin="0,3"
BorderThickness="1"
CornerRadius="3"
BorderBrush="#CC000000"
Background="#44000000">
<Border.LayoutTransform>
<ScaleTransform ScaleX="0" ScaleY="0" />
</Border.LayoutTransform>
<StackPanel Margin="5">
<TextBlock Text="Types"
HorizontalAlignment="Center"
FontSize="14"
Margin="0,0,0,5"
Foreground="#EEFFFFFF"/>
<ListBox ItemsSource="{Binding CompatibleTypes}"
SelectedItem="{Binding SelectedCompatibleType, Mode=TwoWay}"
Background="Transparent"
BorderBrush="Transparent"
MaxHeight="250">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Grid>
<Border x:Name="selectedTypeBackground"
Background="#99FFFFFF"
BorderThickness="1"
BorderBrush="#DDFFFFFF"
CornerRadius="3"
Visibility="Hidden"/>
<ContentPresenter Margin="3" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected"
Value="True">
<Setter TargetName=
"selectedTypeBackground"
Property="Visibility"
Value="Visible" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</StackPanel>
</Border>
</Grid>
</Grid>
<DataTemplate.Triggers>
...
<Trigger SourceName="typesToggle"
Property="IsChecked"
Value="True">
<Setter TargetName="typesToggle"
Property="ToolTip"
Value="Collapse the compatible types region." />
<Trigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource showTypesStoryboard}"/>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard Storyboard="{StaticResource hideTypesStoryboard}" />
</Trigger.ExitActions>
</Trigger>
</DataTemplate.Triggers>
</DataTemplate>
For the Remove button, we use a SuspendedButton, in order to run an animation before the field is removed.
In the compatible types region, we have a ListBox
for selecting the value's type.
In the field's region, we have a TextBlock
for presenting the field's name, a ToggleButton
that determines if the value is null
and, a ContentControl
for presenting the field's value.
In order to present different types in different ways, we create data-templates for the types:
<DataTemplate x:Key="regularFieldDataTemplate"
DataType="{x:Type local:ValueViewModel}">
...
</DataTemplate>
<DataTemplate x:Key="booleanFieldDataTemplate"
DataType="{x:Type local:ValueViewModel}">
...
</DataTemplate>
<DataTemplate x:Key="enumFieldDataTemplate"
DataType="{x:Type local:ValueViewModel}">
...
</DataTemplate>
<DataTemplate x:Key="complexFieldDataTemplate"
DataType="{x:Type local:ValueViewModel}">
...
</DataTemplate>
<DataTemplate x:Key="collectionFieldDataTemplate"
DataType="{x:Type local:ValueViewModel}">
...
</DataTemplate>
<DataTemplate x:Key="nullFieldDataTemplate"
DataType="{x:Type local:ValueViewModel}">
...
</DataTemplate>
and, change the ContentTemplate
of the value's ContentControl
appropriately:
<DataTemplate DataType="{x:Type local:ValueViewModel}">
...
<DataTemplate.Triggers>
...
<DataTrigger Binding="{Binding IsString}"
Value="True">
<Setter TargetName="fieldValue"
Property="ContentTemplate"
Value="{StaticResource regularFieldDataTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding IsParsable}"
Value="True">
<Setter TargetName="fieldValue"
Property="ContentTemplate"
Value="{StaticResource regularFieldDataTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding HasSubFields}"
Value="True">
<Setter TargetName="fieldValue"
Property="ContentTemplate"
Value="{StaticResource complexFieldDataTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding IsCollection}"
Value="True">
<Setter TargetName="fieldValue"
Property="ContentTemplate"
Value="{StaticResource collectionFieldDataTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding IsEnum}"
Value="True">
<Setter TargetName="fieldValue"
Property="ContentTemplate"
Value="{StaticResource enumFieldDataTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding IsBoolean}"
Value="True">
<Setter TargetName="fieldValue"
Property="ContentTemplate"
Value="{StaticResource booleanFieldDataTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding IsNull}"
Value="True">
<Setter TargetName="fieldValue"
Property="ContentTemplate"
Value="{StaticResource nullFieldDataTemplate}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
Additional data-templates
In order to support different data-templates for specific types, we have to give the user a way to define specific data-templates for the wanted types. We can do that by holding the types' data-templates in ValueViewModel
:
public class TypeDataTemplate : BaseViewModel
{
#region Properties
#region ValueType
private Type _valueType;
public Type ValueType
{
get { return _valueType; }
set
{
if (_valueType != value)
{
_valueType = value;
NotifyPropertyChanged("ValueType");
}
}
}
#endregion
#region ValueViewModelDataTemplate
private DataTemplate _valueViewModelDataTemplate;
public DataTemplate ValueViewModelDataTemplate
{
get { return _valueViewModelDataTemplate; }
set
{
if (_valueViewModelDataTemplate != value)
{
_valueViewModelDataTemplate = value;
NotifyPropertyChanged("ValueViewModelDataTemplate");
}
}
}
#endregion
#endregion
}
public abstract class ValueViewModel : BaseViewModel
{
...
#region DataTemplates
private IEnumerable<TypeDataTemplate> _dataTemplates;
public IEnumerable<TypeDataTemplate> DataTemplates
{
get { return _dataTemplates; }
set
{
if (_dataTemplates != value)
{
IEnumerable<TypeDataTemplate> oldValue = _dataTemplates;
_dataTemplates = value;
OnDataTemplatesChanged(oldValue, _dataTemplates);
NotifyPropertyChanged("DataTemplates");
}
}
}
protected virtual void OnDataTemplatesChanged
(IEnumerable<TypeDataTemplate> oldValue, IEnumerable<TypeDataTemplate> newValue)
{
}
#endregion
...
}
Setting the appropriate data-template (if there is) according to the SelectedCompatibleType
:
protected virtual void OnDataTemplatesChanged(IEnumerable<TypeDataTemplate> oldValue,
IEnumerable<TypeDataTemplate> newValue)
{
ValueDataTemplate = null;
if (newValue != null)
{
Type t = SelectedCompatibleType;
if (t != null)
{
ValueDataTemplate = newValue.Where(tdt1 => tdt1.ValueType == t).
Select(tdt2 => tdt2.ValueViewModelDataTemplate).FirstOrDefault();
}
foreach (ValueViewModel subField in SubFields)
{
subField.DataTemplates = newValue;
}
foreach (ValueViewModel elem in CollectionElements)
{
elem.DataTemplates = newValue;
}
}
}
#region ValueDataTemplate
private DataTemplate _valueDataTemplate;
public DataTemplate ValueDataTemplate
{
get { return _valueDataTemplate; }
set
{
if (_valueDataTemplate != value)
{
_valueDataTemplate = value;
NotifyPropertyChanged("ValueDataTemplate");
NotifyPropertyChanged("HasValueDataTemplate");
}
}
}
#endregion
#region HasValueDataTemplate
public bool HasValueDataTemplate
{
get { return ValueDataTemplate != null; }
}
#endregion
and, adding a DataTrigger
for setting the appropriate DataTemplate
:
<DataTemplate DataType="{x:Type local:ValueViewModel}">
...
<DataTemplate.Triggers>
...
<DataTrigger Binding="{Binding HasValueDataTemplate}"
Value="True">
<Setter TargetName="fieldValue"
Property="ContentTemplate"
Value="{Binding ValueDataTemplate}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
Buttons styles
In addition to the data-templates, we add styles for some of the buttons.
For the Remove button, we add the following style ():
<Style x:Key="removeButtonStyle" TargetType="{x:Type Button}">
<Setter Property="BorderThickness"
Value="1" />
<Setter Property="BorderBrush"
Value="DarkRed" />
<Setter Property="Background"
Value="Red" />
<Setter Property="Foreground"
Value="White" />
<Setter Property="Width"
Value="15" />
<Setter Property="Height"
Value="15" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<ControlTemplate.Resources>
<Storyboard x:Key="mouseEnterStoryboard">
<DoubleAnimation Storyboard.TargetProperty="Opacity"
Storyboard.TargetName="rootElement"
To="1"
Duration="0:0:0.05" />
</Storyboard>
<Storyboard x:Key="mouseLeaveStoryboard">
<DoubleAnimation Storyboard.TargetProperty="Opacity"
Storyboard.TargetName="rootElement"
To="0.5"
Duration="0:0:0.15" />
</Storyboard>
</ControlTemplate.Resources>
<Grid x:Name="rootElement"
Opacity="0.5">
<Ellipse Stroke="{TemplateBinding BorderBrush}"
StrokeThickness="1"
Fill="{TemplateBinding Background}" />
<Line X1="1" Y1="1" X2="7" Y2="7"
Stroke="{TemplateBinding Foreground}"
StrokeThickness="2"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
<Line X1="1" Y1="7" X2="7" Y2="1"
Stroke="{TemplateBinding Foreground}"
StrokeThickness="2"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver"
Value="True">
<Trigger.EnterActions>
<BeginStoryboard Storyboard=
"{StaticResource mouseEnterStoryboard}" />
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard Storyboard=
"{StaticResource mouseLeaveStoryboard}" />
</Trigger.ExitActions>
</Trigger>
<Trigger Property="IsPressed"
Value="True">
<Setter Property="Background"
Value="#FFCC0000" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
for the Expand button, we add the following style ():
<Style x:Key="expandButtonStyle" TargetType="{x:Type ToggleButton}">
<Setter Property="BorderThickness"
Value="1" />
<Setter Property="BorderBrush"
Value="DarkBlue" />
<Setter Property="Background"
Value="LightBlue" />
<Setter Property="Foreground"
Value="Cyan" />
<Setter Property="Width"
Value="12" />
<Setter Property="Height"
Value="12" />
<Setter Property="Padding"
Value="0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<ControlTemplate.Resources>
<Storyboard x:Key="mouseEnterStoryboard">
<DoubleAnimation Storyboard.TargetProperty="Opacity"
Storyboard.TargetName="rootElement"
To="1"
Duration="0:0:0.05" />
</Storyboard>
<Storyboard x:Key="mouseLeaveStoryboard">
<DoubleAnimation Storyboard.TargetProperty="Opacity"
Storyboard.TargetName="rootElement"
To="0.5"
Duration="0:0:0.15" />
</Storyboard>
<Storyboard x:Key="checkedStoryboard">
<DoubleAnimation Storyboard.TargetName="mainShape"
Storyboard.TargetProperty=
"(UIElement.RenderTransform).(RotateTransform.Angle)"
To="45"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Key="uncheckedStoryboard">
<DoubleAnimation Storyboard.TargetName="mainShape"
Storyboard.TargetProperty=
"(UIElement.RenderTransform).(RotateTransform.Angle)"
To="-45"
Duration="0:0:0.2" />
</Storyboard>
</ControlTemplate.Resources>
<Grid x:Name="rootElement"
Opacity="0.5">
<Polygon x:Name="mainShape"
Stroke="{TemplateBinding BorderBrush}"
StrokeThickness="2"
StrokeLineJoin="Round"
Fill="{TemplateBinding Background}"
RenderTransformOrigin="0.5,0.5"
Points="0,10 10,10 10,0">
<Polygon.RenderTransform>
<RotateTransform Angle="-45" />
</Polygon.RenderTransform>
</Polygon>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver"
Value="True">
<Trigger.EnterActions>
<BeginStoryboard Storyboard=
"{StaticResource mouseEnterStoryboard}" />
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard Storyboard=
"{StaticResource mouseLeaveStoryboard}" />
</Trigger.ExitActions>
</Trigger>
<Trigger Property="IsPressed"
Value="True">
<Setter Property="Background"
Value="#FF0000CC" />
</Trigger>
<Trigger Property="IsChecked"
Value="True">
<Trigger.EnterActions>
<BeginStoryboard Storyboard=
"{StaticResource checkedStoryboard}" />
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard Storyboard=
"{StaticResource uncheckedStoryboard}" />
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
and, for the Null button, we add the following style ():
<Style x:Key="nullButtonStyle" TargetType="{x:Type ToggleButton}">
<Setter Property="BorderThickness"
Value="1" />
<Setter Property="BorderBrush"
Value="#CC000000" />
<Setter Property="Background"
Value="#44000000" />
<Setter Property="Foreground"
Value="White" />
<Setter Property="Width"
Value="20" />
<Setter Property="Height"
Value="20" />
<Setter Property="Padding"
Value="0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<ControlTemplate.Resources>
<Storyboard x:Key="mouseEnterStoryboard">
<DoubleAnimation Storyboard.TargetProperty="Opacity"
Storyboard.TargetName="backgroundBorder"
To="1"
Duration="0:0:0.05" />
</Storyboard>
<Storyboard x:Key="mouseLeaveStoryboard">
<DoubleAnimation Storyboard.TargetProperty="Opacity"
Storyboard.TargetName="backgroundBorder"
To="0.5"
Duration="0:0:0.15" />
</Storyboard>
</ControlTemplate.Resources>
<Grid >
<Border x:Name="backgroundBorder"
Opacity="0.5"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
CornerRadius="2"/>
<Grid x:Name="nullSign"
Opacity="0.5">
<Ellipse Stroke="{TemplateBinding Foreground}"
StrokeThickness="2"
Fill="Transparent"
Margin="4" />
<Line X1="0" Y1="14" X2="14" Y2="0"
Stroke="{TemplateBinding Foreground}"
StrokeThickness="2"
Margin="3" />
</Grid>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver"
Value="True">
<Trigger.EnterActions>
<BeginStoryboard Storyboard=
"{StaticResource mouseEnterStoryboard}" />
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard Storyboard=
"{StaticResource mouseLeaveStoryboard}" />
</Trigger.ExitActions>
</Trigger>
<Trigger Property="IsPressed"
Value="True">
<Setter Property="Background"
Value="#AA000000" />
</Trigger>
<Trigger Property="IsChecked"
Value="True">
<Setter TargetName="nullSign"
Property="Opacity"
Value="1" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Generate a value from the inputs
In order to get the parameter's value from the whole of the value's parts (e.g. collection's elements, class properties, etc.), we have to go over each part and generate its value appropriately. That can be done as the following:
protected virtual object GetValue()
{
if (IsNullable && IsNull)
{
return null;
}
if (IsString && _value == null)
{
return string.Empty;
}
if (IsParsable)
{
return ParseIfNeeded(_value);
}
if (IsCollection)
{
return GenerateValueFromCollectionElements();
}
if (HasSubFields)
{
return GenerateValueFromSubFields();
}
if (IsEnum)
{
return GenerateEnumValue();
}
return _value;
}
In the ParseIfNeeded
method, we get the value using the Parse
method of the value's type.
In the GenerateValueFromCollectionElements
method, we create a collection (according to the collection's type), get the values of the collection's elements and, set the values in the collection.
In the GenerateValueFromSubFields
method, we create an object (according to the value's type) and, set its fields and properties according to the SubFields
.
In the GenerateEnumValue
method, we get the Enum
value that is compatible to the input's string
representation.
Handling Methods
Presenting Methods
After we have a view-model for getting values, we can get the parameters of a method and, invoke that method with the gotten parameters. For that purpose, we create a control that presents a method:
public class MethodPresenter : Control
{
static MethodPresenter()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MethodPresenter),
new FrameworkPropertyMetadata(typeof(MethodPresenter)));
}
}
To this control, we add properties for the method's name:
#region MethodName
public string MethodName
{
get { return (string)GetValue(MethodNameProperty); }
protected set { SetValue(MethodNameProperty, value); }
}
public static readonly DependencyProperty MethodNameProperty =
DependencyProperty.Register("MethodName", typeof(string),
typeof(MethodPresenter), new UIPropertyMetadata(null));
#endregion
The object on which to invoke the method:
#region ObjectInstance
public object ObjectInstance
{
get { return (object)GetValue(ObjectInstanceProperty); }
set { SetValue(ObjectInstanceProperty, value); }
}
public static readonly DependencyProperty ObjectInstanceProperty =
DependencyProperty.Register("ObjectInstance", typeof(object),
typeof(MethodPresenter), new UIPropertyMetadata(null));
#endregion
and, properties to support derived types and additional data-templates:
#region KnownTypes
public IEnumerable<Type> KnownTypes
{
get { return (IEnumerable<Type>)GetValue(KnownTypesProperty); }
set { SetValue(KnownTypesProperty, value); }
}
public static readonly DependencyProperty KnownTypesProperty =
DependencyProperty.Register("KnownTypes", typeof(IEnumerable<Type>),
typeof(MethodPresenter), new UIPropertyMetadata(null));
#endregion
#region AutoGenerateCompatibleTypes
public bool AutoGenerateCompatibleTypes
{
get { return (bool)GetValue(AutoGenerateCompatibleTypesProperty); }
set { SetValue(AutoGenerateCompatibleTypesProperty, value); }
}
public static readonly DependencyProperty AutoGenerateCompatibleTypesProperty =
DependencyProperty.Register("AutoGenerateCompatibleTypes", typeof(bool),
typeof(MethodPresenter), new UIPropertyMetadata(true));
#endregion
#region DataTemplates
public IEnumerable<TypeDataTemplate> DataTemplates
{
get { return (IEnumerable<TypeDataTemplate>)GetValue(DataTemplatesProperty); }
set { SetValue(DataTemplatesProperty, value); }
}
public static readonly DependencyProperty DataTemplatesProperty =
DependencyProperty.Register("DataTemplates", typeof(IEnumerable<TypeDataTemplate>),
typeof(MethodPresenter), new UIPropertyMetadata(null));
#endregion
For getting the method's parameters, we add a property for holding the method's parameters:
#region MethodParameters
private ObservableCollection<InputValueViewModel> _methodParameters;
public ObservableCollection<InputValueViewModel> MethodParameters
{
get
{
return _methodParameters ??
(_methodParameters = new ObservableCollection<InputValueViewModel>());
}
}
#endregion
Create a view-model for getting a method's parameter:
public class ParameterInputValueViewModel : InputValueViewModel
{
public ParameterInputValueViewModel(ParameterInfo pi)
{
ParameterInformation = pi;
}
#region Properties
#region ParameterInformation
private ParameterInfo _parameterInformation;
public ParameterInfo ParameterInformation
{
protected get { return _parameterInformation; }
set
{
if (_parameterInformation != value)
{
ParameterInfo oldValue = _parameterInformation;
_parameterInformation = value;
OnParameterInformationChanged(oldValue, _parameterInformation);
}
}
}
protected virtual void OnParameterInformationChanged
(ParameterInfo oldValue, ParameterInfo newValue)
{
if (newValue == null)
{
return;
}
Name = newValue.Name;
ValueType = newValue.ParameterType;
}
#endregion
#endregion
}
and, set the method's parameters according to a given method's information:
public MethodInfo MethodInformation
{
get { return (MethodInfo)GetValue(MethodInformationProperty); }
set { SetValue(MethodInformationProperty, value); }
}
public static readonly DependencyProperty MethodInformationProperty =
DependencyProperty.Register("MethodInformation", typeof(MethodInfo),
typeof(MethodPresenter), new UIPropertyMetadata(null, OnMethodInformationChanged));
private static void OnMethodInformationChanged
(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
MethodPresenter mp = sender as MethodPresenter;
if (mp == null)
{
return;
}
mp.MethodParameters.Clear();
if (mp.MethodInformation != null)
{
mp.MethodName = mp.MethodInformation.Name;
mp.MethodInformation.GetParameters().ToList().
ForEach(pi => mp.MethodParameters.Add(
new ParameterInputValueViewModel(pi)
{
KnownTypes = mp.KnownTypes,
AutoGenerateCompatibleTypes = mp.AutoGenerateCompatibleTypes,
DataTemplates = mp.DataTemplates
}));
}
else
{
mp.MethodName = null;
}
}
Invoking methods
After we have the method's parameters, we have to invoke the method. For that purpose, we add a RoutedCommand
:
private static RoutedCommand _invokeMethodCommand;
public static RoutedCommand InvokeMethodCommand
{
get
{
return _invokeMethodCommand ??
(_invokeMethodCommand = new RoutedCommand
("InvokeMethod", typeof(MethodPresenter)));
}
}
add CanExecute
and Executed
event-handlers:
private static void CanExecuteInvokeMethodCommand
(object sender, CanExecuteRoutedEventArgs e)
{
MethodPresenter mp = sender as MethodPresenter;
if (mp == null)
{
return;
}
e.CanExecute = mp.MethodInformation != null && mp.ObjectInstance != null;
}
private static void ExecuteInvokeMethodCommand(object sender, ExecutedRoutedEventArgs e)
{
MethodPresenter mp = sender as MethodPresenter;
if (mp == null)
{
return;
}
mp.InvokeMethod();
}
private void InvokeMethod()
{
MethodInfo mi = MethodInformation;
object obj = ObjectInstance;
if (mi == null || obj == null)
{
return;
}
object[] parameters = MethodParameters.Select(p => p.Value).ToArray();
_invokeMethodThread = new Thread(() =>
{
OutputValueViewModel methodReturnValue = null;
OutputValueViewModel methodException = null;
try
{
object retVal = mi.Invoke(obj, parameters);
methodReturnValue = mi.ReturnType != typeof(void) ?
new OutputValueViewModel(retVal) : null;
}
catch (System.Reflection.TargetInvocationException tie)
{
methodException = new OutputValueViewModel(tie.InnerException);
}
catch (Exception ex)
{
methodException = new OutputValueViewModel(ex);
}
finally
{
Dispatcher.BeginInvoke(DispatcherPriority.Normal,
new ThreadStart(() =>
{
if (methodReturnValue != null)
{
methodReturnValue.DataTemplates = DataTemplates;
}
if (methodException != null)
{
methodException.DataTemplates = DataTemplates;
}
MethodResultViewModel methodResult = new MethodResultViewModel
{
MethodName = mi.Name,
ResultTime = DateTime.Now,
MethodReturnValue = methodReturnValue,
MethodException = methodException
};
int paramInx = 0;
foreach (ParameterInfo pi in mi.GetParameters())
{
if (pi.IsOut || pi.ParameterType.IsByRef)
{
methodResult.MethodOutputs.Add(
new ParameterOutputValueViewModel
(pi, parameters[paramInx])
{
DataTemplates = DataTemplates
});
}
paramInx++;
}
methodResult.Removed += OnMethodResultRemoved;
methodResult.Shown += OnMethodResultShown;
if (StoreMethodResults)
{
MethodResults.Add(methodResult);
HasResults = true;
}
CurrentMethodResult = methodResult;
}));
}
_invokeMethodThread = null;
});
_invokeMethodThread.Start();
}
private Thread _invokeMethodThread;
#region MethodResults
private ObservableCollection<MethodResultViewModel> _methodResults;
public ObservableCollection<MethodResultViewModel> MethodResults
{
get
{
return _methodResults ?? (_methodResults =
new ObservableCollection<MethodResultViewModel>());
}
}
#endregion
#region CurrentMethodResult
public MethodResultViewModel CurrentMethodResult
{
get { return (MethodResultViewModel)GetValue(CurrentMethodResultProperty); }
set { SetValue(CurrentMethodResultProperty, value); }
}
public static readonly DependencyProperty CurrentMethodResultProperty =
DependencyProperty.Register("CurrentMethodResult", typeof(MethodResultViewModel),
typeof(MethodPresenter), new UIPropertyMetadata(null));
#endregion
#region HasResults
public bool HasResults
{
get { return (bool)GetValue(HasResultsProperty); }
protected set { SetValue(HasResultsProperty, value); }
}
public static readonly DependencyProperty HasResultsProperty =
DependencyProperty.Register("HasResults", typeof(bool),
typeof(MethodPresenter), new UIPropertyMetadata(false));
#endregion
#region StoreMethodResults
public bool StoreMethodResults
{
get { return (bool)GetValue(StoreMethodResultsProperty); }
set { SetValue(StoreMethodResultsProperty, value); }
}
public static readonly DependencyProperty StoreMethodResultsProperty =
DependencyProperty.Register("StoreMethodResults", typeof(bool),
typeof(MethodPresenter), new UIPropertyMetadata(false));
#endregion
and, register the event-handlers with the command:
static MethodPresenter()
{
...
CommandBinding invokeMethodBinding = new CommandBinding(InvokeMethodCommand,
ExecuteInvokeMethodCommand, CanExecuteInvokeMethodCommand);
CommandManager.RegisterClassCommandBinding
(typeof(MethodPresenter), invokeMethodBinding);
}
In the InvokeMethod
method, we get the method's parameters, invoke the method in a different thread (in order to prevent UI blocking) and, set the properties that influence on the UI elements, using the UI Dispatcher
.
For presenting the result of the method, we use the MethodResultViewModel
class. This class contains: properties for the result's details, a command for showing the result (this command fires the Shown
event) and, a command for removing the result (this command fires the Removed
event).
For notifying the user about the method's invoke, we add a RoutedEvent
for raising before the method's invoke:
public class MethodInvokeRequestedRoutedEventArgs : RoutedEventArgs
{
#region Constructors
public MethodInvokeRequestedRoutedEventArgs()
{
}
public MethodInvokeRequestedRoutedEventArgs(RoutedEvent routedEvent)
: base(routedEvent)
{
}
public MethodInvokeRequestedRoutedEventArgs(RoutedEvent routedEvent, object source)
: base(routedEvent, source)
{
}
#endregion
public MethodInfo MethodInformation { get; set; }
public object[] Parameters { get; set; }
}
public delegate void MethodInvokeRequestedRoutedEventHandler
(object sender, MethodInvokeRequestedRoutedEventArgs e);
public class MethodPresenter : Control
{
...
public static readonly RoutedEvent MethodInvokeRequestedEvent =
EventManager.RegisterRoutedEvent("MethodInvokeRequested", RoutingStrategy.Bubble,
typeof(MethodInvokeRequestedRoutedEventHandler), typeof(MethodPresenter));
public event MethodInvokeRequestedRoutedEventHandler MethodInvokeRequested
{
add { AddHandler(MethodInvokeRequestedEvent, value); }
remove { RemoveHandler(MethodInvokeRequestedEvent, value); }
}
...
}
add a RoutedEvent
for raising after the method has been invoked:
public class MethodInvokedRoutedEventArgs : RoutedEventArgs
{
#region Constructors
public MethodInvokedRoutedEventArgs()
{
}
public MethodInvokedRoutedEventArgs(RoutedEvent routedEvent)
: base(routedEvent)
{
}
public MethodInvokedRoutedEventArgs(RoutedEvent routedEvent, object source)
: base(routedEvent, source)
{
}
#endregion
public MethodInfo MethodInformation { get; set; }
public MethodResultViewModel MethodResult { get; set; }
}
public delegate void MethodInvokedRoutedEventHandler
(object sender, MethodInvokedRoutedEventArgs e);
public class MethodPresenter : Control
{
...
public static readonly RoutedEvent MethodInvokedEvent =
EventManager.RegisterRoutedEvent("MethodInvoked", RoutingStrategy.Bubble,
typeof(MethodInvokedRoutedEventHandler), typeof(MethodPresenter));
public event MethodInvokedRoutedEventHandler MethodInvoked
{
add { AddHandler(MethodInvokedEvent, value); }
remove { RemoveHandler(MethodInvokedEvent, value); }
}
...
}
and, add a property for indicating a method's invoke process:
public bool IsInvoking
{
get { return (bool)GetValue(IsInvokingProperty); }
protected set { SetValue(IsInvokingProperty, value); }
}
public static readonly DependencyProperty IsInvokingProperty =
DependencyProperty.Register("IsInvoking", typeof(bool),
typeof(MethodPresenter), new UIPropertyMetadata(false));
We raise the MethodInvokeRequested
and MethodInvoked
events and set the IsInvoking
property, in the InvokeMethod
method appropriately.
For stoping a method's invoke, we add a RoutedCommand
:
private static RoutedCommand _stopInvokeMethodCommand;
public static RoutedCommand StopInvokeMethodCommand
{
get
{
return _stopInvokeMethodCommand ??
(_stopInvokeMethodCommand = new RoutedCommand
("StopInvokeMethod", typeof(MethodPresenter)));
}
}
and, implement it to abort the thread of the method's invoke:
private static void CanExecuteStopInvokeMethodCommand
(object sender, CanExecuteRoutedEventArgs e)
{
MethodPresenter mp = sender as MethodPresenter;
if (mp == null)
{
return;
}
e.CanExecute = mp.IsInvoking;
}
private static void ExecuteStopInvokeMethodCommand
(object sender, ExecutedRoutedEventArgs e)
{
MethodPresenter mp = sender as MethodPresenter;
if (mp == null)
{
return;
}
mp.StopInvokeMethod();
}
private void StopInvokeMethod()
{
if (_invokeMethodThread != null)
{
_invokeMethodThread.Abort();
_invokeMethodThread = null;
}
IsInvoking = false;
}
static MethodPresenter()
{
...
CommandBinding stopInvokeMethodBinding = new CommandBinding(StopInvokeMethodCommand,
ExecuteStopInvokeMethodCommand, CanExecuteStopInvokeMethodCommand);
CommandManager.RegisterClassCommandBinding
(typeof(MethodPresenter), stopInvokeMethodBinding);
}
In the InvokeMethod
method, we handle the ThreadAbortException
as the following:
private void InvokeMethod()
{
...
bool isAborted = false;
try
{
object retVal = mi.Invoke(obj, parameters);
methodReturnValue = mi.ReturnType != typeof(void) ?
new OutputValueViewModel(retVal) : null;
}
catch (ThreadAbortException tae)
{
isAborted = true;
}
...
finally
{
if (!isAborted)
{
...
}
}
...
}
MethodPresenter style
After we have the MethodPresenter
, we create a default style for presenting this control:
<Style TargetType="{x:Type local:MethodPresenter}">
<Setter Property="Background" Value="LightGreen" />
<Setter Property="BorderBrush" Value="DarkGreen" />
<Setter Property="BorderThickness" Value="2" />
<Setter Property="Foreground" Value="Black" />
<Setter Property="Padding" Value="15" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MethodPresenter}">
<Grid>
<Border BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="10"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}">
<DockPanel>
</DockPanel>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
In the ControlTemplate
, we put a TextBlock
for the method's name:
<TextBlock Text="{TemplateBinding MethodName}"
DockPanel.Dock="Top"
FontSize="20"
Foreground="DarkBlue"
HorizontalAlignment="Center"
Margin="5" />
an ItemsControl
for the method's parameters:
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding MethodParameters,
RelativeSource={RelativeSource Mode=TemplatedParent}}" />
</ScrollViewer>
a Button
for invoking the method:
<Button Content="Invoke" DockPanel.Dock="Bottom"
Command="{x:Static local:MethodPresenter.InvokeMethodCommand}"
HorizontalAlignment="Center"
Margin="5" />
a Grid
for the method's results:
<DataTemplate x:Key="methodResultItemDataTemplate">
...
</DataTemplate>
<ControlTemplate TargetType="{x:Type local:MethodPresenter}">
<ControlTemplate.Resources>
<Storyboard x:Key="showResultsStoryboard">
<DoubleAnimation Storyboard.TargetName="resultsBorder"
Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).
(ScaleTransform.ScaleX)"
To="1"
Duration="0:0:0.2" />
<DoubleAnimation Storyboard.TargetName="resultsBorder"
Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).
(ScaleTransform.ScaleY)"
To="1"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Key="hideResultsStoryboard">
<DoubleAnimation Storyboard.TargetName="resultsBorder"
Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).
(ScaleTransform.ScaleX)"
To="0"
Duration="0:0:0.2" />
<DoubleAnimation Storyboard.TargetName="resultsBorder"
Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).
(ScaleTransform.ScaleY)"
To="0"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Key="showResultsRegionStoryboard">
<DoubleAnimation Storyboard.TargetName="resultsRegion"
Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).
(ScaleTransform.ScaleX)"
To="1"
Duration="0:0:0.2" />
<DoubleAnimation Storyboard.TargetName="resultsRegion"
Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).
(ScaleTransform.ScaleY)"
To="1"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Key="hideResultsRegionStoryboard">
<DoubleAnimation Storyboard.TargetName="resultsRegion"
Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).
(ScaleTransform.ScaleX)"
To="0"
Duration="0:0:0.2" />
<DoubleAnimation Storyboard.TargetName="resultsRegion"
Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).
(ScaleTransform.ScaleY)"
To="0"
Duration="0:0:0.2" />
</Storyboard>
</ControlTemplate.Resources>
...
<Grid DockPanel.Dock="Bottom" x:Name="resultsRegion">
<Grid.LayoutTransform>
<ScaleTransform ScaleX="0" ScaleY="0" />
</Grid.LayoutTransform>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<Button x:Name="btnClear"
Style="{StaticResource removeButtonStyle}"
VerticalAlignment="Center"
Command="{x:Static local:MethodPresenter.
ClearMethodResultsCommand}"
ToolTip="Clear the results."
Margin="2,2,4,2"/>
<ToggleButton x:Name="resultsToggle"
Style="{StaticResource expandButtonStyle}"
VerticalAlignment="Center"
ToolTip="Expand the results region." />
</StackPanel>
<Border x:Name="resultsBorder"
Grid.Row="1"
Margin="0,3,0,0"
BorderThickness="1"
CornerRadius="3"
BorderBrush="#CC000000"
Background="#44000000">
<Border.LayoutTransform>
<ScaleTransform ScaleX="0" ScaleY="0" />
</Border.LayoutTransform>
<StackPanel Margin="5">
<TextBlock Text="Results:"
HorizontalAlignment="Left"
FontSize="14"
Margin="0,0,0,5"
Foreground="#EEFFFFFF"/>
<ScrollViewer MaxHeight="100" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding MethodResults,
RelativeSource={RelativeSource Mode=TemplatedParent}}"
ItemTemplate=
"{StaticResource methodResultItemDataTemplate}" />
</ScrollViewer>
</StackPanel>
</Border>
</Grid>
...
<ControlTemplate.Triggers>
<Trigger SourceName="resultsToggle"
Property="IsChecked"
Value="True">
<Setter TargetName="resultsToggle"
Property="ToolTip"
Value="Collapse the results region." />
<Trigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource showResultsStoryboard}"/>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard Storyboard="{StaticResource hideResultsStoryboard}" />
</Trigger.ExitActions>
</Trigger>
<Trigger Property="HasResults"
Value="True">
<Trigger.EnterActions>
<BeginStoryboard Storyboard=
"{StaticResource showResultsRegionStoryboard}"/>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard Storyboard=
"{StaticResource hideResultsRegionStoryboard}" />
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
and, a Border
for indicating a method's invoke:
<ControlTemplate TargetType="{x:Type local:MethodPresenter}">
...
<Border Name="invokeDisplay"
CornerRadius="10"
Background="#66000000"
Visibility="Hidden">
<Grid VerticalAlignment="Center"
HorizontalAlignment="Center" >
<Border Background="#BB000000">
<Border.Effect>
<BlurEffect Radius="15" />
</Border.Effect>
</Border>
<StackPanel Margin="10">
<TextBlock Foreground="LightGreen"
FontSize="16"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Text="Invoking..." />
<Button Content="Stop"
Command="{x:Static local:MethodPresenter.
StopInvokeMethodCommand}"
HorizontalAlignment="Center"
Padding="2"
Margin="5" />
</StackPanel>
</Grid>
</Border>
...
<ControlTemplate.Triggers>
<Trigger Property="IsInvoking" Value="True">
<Setter TargetName="invokeDisplay"
Property="Visibility"
Value="Visible" />
</Trigger>
...
</ControlTemplate.Triggers>
</ControlTemplate>
Handling Interfaces
Presenting interfaces
After we have the MethodPresenter
control, we can invoke the methods of a given interface. For that purpose, we create a control that presents an interface:
public class InterfacePresenter : Control
{
static InterfacePresenter()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(InterfacePresenter),
new FrameworkPropertyMetadata(typeof(InterfacePresenter)));
}
}
To this control, we add a property for holding the object on which to invoke the method:
public object ObjectInstance
{
get { return (object)GetValue(ObjectInstanceProperty); }
set { SetValue(ObjectInstanceProperty, value); }
}
public static readonly DependencyProperty ObjectInstanceProperty =
DependencyProperty.Register("ObjectInstance", typeof(object),
typeof(InterfacePresenter), new UIPropertyMetadata(null));
and, properties to support derived types and additional data-templates:
#region KnownTypes
public IEnumerable<Type> KnownTypes
{
get { return (IEnumerable<Type>)GetValue(KnownTypesProperty); }
set { SetValue(KnownTypesProperty, value); }
}
public static readonly DependencyProperty KnownTypesProperty =
DependencyProperty.Register("KnownTypes", typeof(IEnumerable<Type>),
typeof(InterfacePresenter), new UIPropertyMetadata(null));
#endregion
#region AutoGenerateCompatibleTypes
public bool AutoGenerateCompatibleTypes
{
get { return (bool)GetValue(AutoGenerateCompatibleTypesProperty); }
set { SetValue(AutoGenerateCompatibleTypesProperty, value); }
}
public static readonly DependencyProperty AutoGenerateCompatibleTypesProperty =
DependencyProperty.Register("AutoGenerateCompatibleTypes", typeof(bool),
typeof(InterfacePresenter), new UIPropertyMetadata(true));
#endregion
#region DataTemplates
public IEnumerable<TypeDataTemplate> DataTemplates
{
get { return (IEnumerable<TypeDataTemplate>)GetValue(DataTemplatesProperty); }
set { SetValue(DataTemplatesProperty, value); }
}
public static readonly DependencyProperty DataTemplatesProperty =
DependencyProperty.Register("DataTemplates", typeof(IEnumerable<TypeDataTemplate>),
typeof(InterfacePresenter), new UIPropertyMetadata(null));
#endregion
For getting the interface's methods, we add a property for holding the interface's methods:
private ObservableCollection<MethodInfo> _interfaceMethods;
public ObservableCollection<MethodInfo> InterfaceMethods
{
get
{
return _interfaceMethods ??
(_interfaceMethods = new ObservableCollection<MethodInfo>());
}
}
and, set the interface's methods according to a given interface's type:
public Type InterfaceType
{
get { return (Type)GetValue(InterfaceTypeProperty); }
set { SetValue(InterfaceTypeProperty, value); }
}
public static readonly DependencyProperty InterfaceTypeProperty =
DependencyProperty.Register
("InterfaceType", typeof(Type), typeof(InterfacePresenter),
new UIPropertyMetadata(null,
OnInterfaceTypeChanged), ValidateInterfaceTypeValue);
private static void OnInterfaceTypeChanged
(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
InterfacePresenter ip = sender as InterfacePresenter;
if (ip == null)
{
return;
}
ip.InterfaceMethods.Clear();
Type interfaceType = ip.InterfaceType;
if (interfaceType != null)
{
GetInterfaceMethods(interfaceType).ForEach(mi => ip.InterfaceMethods.Add(mi));
}
}
private static bool ValidateInterfaceTypeValue(object value)
{
if (value == null)
{
return true;
}
Type interfaceType = value as Type;
if (interfaceType != null && interfaceType.IsInterface)
{
return true;
}
return false;
}
private static List<MethodInfo> GetInterfaceMethods(Type interfaceType)
{
List<MethodInfo> methodInfos = new List<MethodInfo>();
AddInterfaceMethods(interfaceType, methodInfos);
return methodInfos;
}
private static void AddInterfaceMethods
(Type interfaceType, List<MethodInfo> methodInfos)
{
foreach (MethodInfo mi in interfaceType.GetMethods())
{
methodInfos.Add(mi);
}
foreach (Type baseInterface in interfaceType.GetInterfaces())
{
AddInterfaceMethods(baseInterface, methodInfos);
}
}
For presenting the method's result, we add a property for holding the current method's result:
public MethodResultViewModel CurrentMethodResult
{
get { return (MethodResultViewModel)GetValue(CurrentMethodResultProperty); }
set { SetValue(CurrentMethodResultProperty, value); }
}
public static readonly DependencyProperty CurrentMethodResultProperty =
DependencyProperty.Register("CurrentMethodResult", typeof(MethodResultViewModel),
typeof(InterfacePresenter), new UIPropertyMetadata
(null, OnCurrentMethodResultChanged));
private static void OnCurrentMethodResultChanged
(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
InterfacePresenter ip = sender as InterfacePresenter;
if (ip == null)
{
return;
}
ip.RaiseEvent(new RoutedEventArgs
(InterfacePresenter.CurrentMethodResultChangedEvent));
}
public static readonly RoutedEvent CurrentMethodResultChangedEvent =
EventManager.RegisterRoutedEvent(
"CurrentMethodResultChanged", RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(InterfacePresenter));
public event RoutedEventHandler MethodInvoked
{
add { AddHandler(CurrentMethodResultChangedEvent, value); }
remove { RemoveHandler(CurrentMethodResultChangedEvent, value); }
}
and, handle the MethodInvoked
event, to set the current result, to the result of the last invoked method:
public InterfacePresenter()
{
AddHandler(MethodPresenter.MethodInvokedEvent,
new MethodInvokedRoutedEventHandler(OnMethodInvoked));
}
private void OnMethodInvoked(object sender, MethodInvokedRoutedEventArgs e)
{
CurrentMethodResult = e.MethodResult;
}
InterfacePresenter style
After we have the InterfacePresenter
, we create a default style for presenting this control:
<Style TargetType="{x:Type local:InterfacePresenter}">
<Setter Property="Background" Value="LightYellow" />
<Setter Property="BorderBrush" Value="Orange" />
<Setter Property="BorderThickness" Value="2" />
<Setter Property="Padding" Value="10" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:InterfacePresenter}">
<DockPanel>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
In the ControlTemplate
, we put a Border
for presenting the interface's methods:
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
CornerRadius="15"
Padding="{TemplateBinding Padding}"
Margin="5">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="Methods:"
Foreground="DarkGreen"
FontSize="14"
FontWeight="Bold"
Margin="0,0,0,5" />
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto"
Margin="0,5,0,0">
<ItemsControl ItemsSource="{Binding InterfaceMethods,
RelativeSource={RelativeSource Mode=TemplatedParent}}" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:MethodPresenter MethodInformation="{Binding}"
ObjectInstance="{Binding DataContext.
ObjectInstance,
ElementName=templatedParentHolder}"
StoreMethodResults="{Binding DataContext.
StoreMethodResults,
ElementName=templatedParentHolder}"
KnownTypes="{Binding DataContext.
KnownTypes,
ElementName=templatedParentHolder}"
AutoGenerateCompatibleTypes=
"{Binding DataContext.
AutoGenerateCompatibleTypes,
ElementName=templatedParentHolder}"
DataTemplates="{Binding DataContext.
DataTemplates,
ElementName=templatedParentHolder}"
Margin="5"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<FrameworkElement x:Name="templatedParentHolder"
DataContext="{Binding RelativeSource=
{RelativeSource Mode=TemplatedParent}}"
Visibility="Collapsed" />
</Grid>
</Border>
and, a Border
for presenting the current result:
<DataTemplate x:Key="methodResultDataTemplate">
...
</DataTemplate>
<ControlTemplate TargetType="{x:Type local:InterfacePresenter}">
...
<Border x:Name="currentMethodResultRegion"
DockPanel.Dock="Bottom"
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="15"
Padding="{TemplateBinding Padding}"
Margin="5"
RenderTransformOrigin="0.5,0.5">
<Border.BorderBrush>
<SolidColorBrush Color="Orange" />
</Border.BorderBrush>
<Border.RenderTransform>
<ScaleTransform />
</Border.RenderTransform>
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<ContentControl Content="{Binding CurrentMethodResult,
RelativeSource={RelativeSource Mode=TemplatedParent}}"
ContentTemplate="{StaticResource methodResultDataTemplate}" />
</ScrollViewer>
</Border>
...
</ControlTemplate>
Handling Objects
Presenting object's properties
In order to support invoking methods that are affected from the object's properties, we need a way to set the object's properties, before the invoke of the method. For that purpose, we create a control that presents object's properties:
public class ValuePresenter : Control
{
static ValuePresenter()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ValuePresenter),
new FrameworkPropertyMetadata(typeof(ValuePresenter)));
}
}
To this control, we add properties for the value and the value's type:
#region Value
public object Value
{
get { return (object)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(object),
typeof(ValuePresenter), new UIPropertyMetadata(null));
#endregion
#region ValueType
public Type ValueType
{
get { return (Type)GetValue(ValueTypeProperty); }
set { SetValue(ValueTypeProperty, value); }
}
public static readonly DependencyProperty ValueTypeProperty =
DependencyProperty.Register("ValueType", typeof(Type),
typeof(ValuePresenter), new UIPropertyMetadata(null));
#endregion
and, add a property for holding a view-model for the value:
public InputValueViewModel PresentedValue
{
get { return (InputValueViewModel)GetValue(PresentedValueProperty); }
protected set { SetValue(PresentedValueProperty, value); }
}
public static readonly DependencyProperty PresentedValueProperty =
DependencyProperty.Register("PresentedValue", typeof(InputValueViewModel),
typeof(ValuePresenter), new UIPropertyMetadata(null));
When the Value
property is changed, we change the value of the value's view-model and vice versa.
In order to present the value that is set to a ValuePresenter
, we have to update each sub view-model of the view-model according to the set value. We can do that as the following:
protected void UpdateValue(object value)
{
object newValue = value;
IsNull = newValue == null;
if (newValue == null)
{
SubFields.Clear();
CollectionElements.Clear();
return;
}
if (SelectedCompatibleType == null || IsCollection || HasSubFields)
{
Type newValueType = newValue != null ? newValue.GetType() : null;
if (ValueType == null)
{
ValueType = newValueType;
}
else if (SelectedCompatibleType != newValueType)
{
SelectedCompatibleType = newValueType;
}
}
if (HasSubFields || IsCollection)
{
UpdateSubFields(newValue);
UpdateCollectionElements(newValue);
}
}
private void UpdateCollectionElements(object newValue)
{
IEnumerable newCollectionValue = newValue as IEnumerable;
if (!IsCollection || newCollectionValue == null)
{
return;
}
int elementIndex = 0;
foreach (object val in newCollectionValue)
{
if (elementIndex >= CollectionElements.Count)
{
AddNewCollectionElement(false);
}
ValueViewModel currElement = CollectionElements[elementIndex];
if (currElement != null)
{
currElement.Value = val;
}
elementIndex++;
}
while (CollectionElements.Count > elementIndex)
{
CollectionElements.RemoveAt(elementIndex);
}
}
private void UpdateSubFields(object newValue)
{
if (newValue == null)
{
return;
}
Type newValueType = newValue.GetType();
foreach (ValueViewModel subField in SubFields)
{
if (subField is PropertyInputValueViewModel)
{
PropertyInfo pi = newValueType.GetProperty(subField.Name);
if (pi != null)
{
subField.Value = pi.GetValue(newValue, null);
}
}
else if (subField is FieldInputValueViewModel)
{
FieldInfo fi = newValueType.GetField(subField.Name);
if (fi != null)
{
subField.Value = fi.GetValue(newValue);
}
}
}
}
In the UpdateValue
method, we change the SelectedCompatibleType
if it is needed and, update the sub view-models of the view-model appropriately.
The default style of the ValuePresenter
control is defined as the following:
<Style TargetType="{x:Type local:ValuePresenter}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ValuePresenter}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ContentControl Content="{TemplateBinding PresentedValue}" />
</ScrollViewer>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Presenting objects
For presenting objects, we create a control:
public class ObjectPresenter : Control
{
static ObjectPresenter()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ObjectPresenter),
new FrameworkPropertyMetadata(typeof(ObjectPresenter)));
}
}
To this control, we add property for holding the object that we want to present:
public object ObjectInstance
{
get { return (object)GetValue(ObjectInstanceProperty); }
set { SetValue(ObjectInstanceProperty, value); }
}
public static readonly DependencyProperty ObjectInstanceProperty =
DependencyProperty.Register("ObjectInstance",
typeof(object), typeof(ObjectPresenter),
new UIPropertyMetadata(null, OnObjectInstanceChanged));
private static void OnObjectInstanceChanged
(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
}
and, properties to support derived types and additional data-templates:
#region KnownTypes
public IEnumerable<Type> KnownTypes
{
get { return (IEnumerable<Type>)GetValue(KnownTypesProperty); }
set { SetValue(KnownTypesProperty, value); }
}
public static readonly DependencyProperty KnownTypesProperty =
DependencyProperty.Register("KnownTypes", typeof(IEnumerable<Type>),
typeof(ObjectPresenter), new UIPropertyMetadata(null));
#endregion
#region AutoGenerateCompatibleTypes
public bool AutoGenerateCompatibleTypes
{
get { return (bool)GetValue(AutoGenerateCompatibleTypesProperty); }
set { SetValue(AutoGenerateCompatibleTypesProperty, value); }
}
public static readonly DependencyProperty AutoGenerateCompatibleTypesProperty =
DependencyProperty.Register("AutoGenerateCompatibleTypes", typeof(bool),
typeof(ObjectPresenter), new UIPropertyMetadata(true));
#endregion
#region DataTemplates
public IEnumerable<TypeDataTemplate> DataTemplates
{
get { return (IEnumerable<TypeDataTemplate>)GetValue(DataTemplatesProperty); }
set { SetValue(DataTemplatesProperty, value); }
}
public static readonly DependencyProperty DataTemplatesProperty =
DependencyProperty.Register("DataTemplates", typeof(IEnumerable<TypeDataTemplate>),
typeof(ObjectPresenter), new UIPropertyMetadata(null));
#endregion
The default style of the ObjectPresenter
control is defined as the following:
<Style TargetType="{x:Type local:ObjectPresenter}">
<Setter Property="Background" Value="LightYellow" />
<Setter Property="BorderBrush" Value="Orange" />
<Setter Property="BorderThickness" Value="2" />
<Setter Property="Padding" Value="10" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ObjectPresenter}">
<DockPanel>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="15"
Padding="{TemplateBinding Padding}"
Margin="5"
DockPanel.Dock="Right">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="Fields:"
Foreground="DarkGreen"
FontSize="14"
FontWeight="Bold"
Margin="0,0,0,5" />
<ScrollViewer Margin="0,5,0,0"
VerticalScrollBarVisibility="Auto"
Grid.Row="1">
<local:ValuePresenter Value="{Binding ObjectInstance,
RelativeSource={RelativeSource
Mode=TemplatedParent}, Mode=TwoWay}"
KnownTypes="{TemplateBinding
KnownTypes}"
AutoGenerateCompatibleTypes=
"{TemplateBinding
AutoGenerateCompatibleTypes}"
DataTemplates="{TemplateBinding
DataTemplates}" />
</ScrollViewer>
</Grid>
</Border>
<local:InterfacePresenter Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding
BorderThickness}"
Padding="{TemplateBinding Padding}"
InterfaceType=
"{TemplateBinding InterfaceType}"
ObjectInstance="{TemplateBinding
ObjectInstance}"
StoreMethodResults="{TemplateBinding
StoreMethodResults}"
KnownTypes="{TemplateBinding KnownTypes}"
AutoGenerateCompatibleTypes=
"{TemplateBinding
AutoGenerateCompatibleTypes}"
DataTemplates=
"{TemplateBinding DataTemplates}" />
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
In this style, we use the InterfacePresenter
control to present the object's methods and, the ValuePresenter
control to present the object's fields.
For setting the object's methods in the methods' collection of the InterfacePresenter
, we add a TemplatePart
:
[TemplatePart(Name = "PART_InterfacePresenter", Type = typeof(InterfacePresenter))]
public class ObjectPresenter : Control
{
...
}
set the name of the InterfacePresenter
to the name of the TemplatePart
:
<local:InterfacePresenter x:Name="PART_InterfacePresenter"
...
/>
find the InterfacePresenter
according to the name:
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_interfacePresenter = GetTemplateChild("PART_InterfacePresenter")
as InterfacePresenter;
}
private InterfacePresenter _interfacePresenter;
and, update the methods' collection of the InterfacePresenter
, for each time the ObjectInstance
is changed:
private static void OnObjectInstanceChanged
(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
ObjectPresenter op = sender as ObjectPresenter;
if (op == null)
{
return;
}
if (e.OldValue != null && e.NewValue != null &&
e.OldValue.GetType() == e.NewValue.GetType())
{
return;
}
if (op.InterfaceType == null)
{
op.UpdateInterfacePresenterWithObjectInstanceMethods();
}
}
private void UpdateInterfacePresenterWithObjectInstanceMethods()
{
if (_interfacePresenter == null)
{
return;
}
_interfacePresenter.InterfaceMethods.Clear();
if (ObjectInstance == null)
{
return;
}
IEnumerable<methodinfo /> methods = ObjectInstance.GetType().GetMethods();
foreach (MethodInfo mi in methods)
{
if (mi.IsSpecialName)
{
continue;
}
_interfacePresenter.InterfaceMethods.Add(mi);
}
}
How to Use It
Using ValuePresenter
For demonstrating the use of the ValuePresenter
control, we create a window that enables editing properties of a shape.
For that window, we create a base class for holding a shape:
public abstract class MyShape
{
public MyShape()
{
Left = 10;
Top = 10;
FillColor = Colors.LightGreen;
StrokeColor = Colors.Green;
StrokeThickness = 2;
}
public double Left { get; set; }
public double Top { get; set; }
public Color FillColor { get; set; }
public Color StrokeColor { get; set; }
public double StrokeThickness { get; set; }
}
create derived classes for holding rectangle and circle:
public class MyRectangle : MyShape
{
public MyRectangle()
{
Width = 150;
Height = 200;
}
public double Width { get; set; }
public double Height { get; set; }
}
public class MyCircle : MyShape
{
public MyCircle()
{
Diameter = 100.0;
}
public double Diameter { get; set; }
}
create data-templates for presenting the shapes:
<DataTemplate DataType="{x:Type local:MyRectangle}">
<Canvas>
<Rectangle Canvas.Top="{Binding Top}"
Canvas.Left="{Binding Left}"
StrokeThickness="{Binding StrokeThickness}"
Width="{Binding Width}"
Height="{Binding Height}" >
<Rectangle.Fill>
<SolidColorBrush Color="{Binding FillColor}" />
</Rectangle.Fill>
<Rectangle.Stroke>
<SolidColorBrush Color="{Binding StrokeColor}" />
</Rectangle.Stroke>
</Rectangle>
</Canvas>
</DataTemplate>
<DataTemplate DataType="{x:Type local:MyCircle}">
<Canvas>
<Ellipse Canvas.Top="{Binding Top}"
Canvas.Left="{Binding Left}"
StrokeThickness="{Binding StrokeThickness}"
Width="{Binding Diameter}"
Height="{Binding Diameter}" >
<Ellipse.Fill>
<SolidColorBrush Color="{Binding FillColor}" />
</Ellipse.Fill>
<Ellipse.Stroke>
<SolidColorBrush Color="{Binding StrokeColor}" />
</Ellipse.Stroke>
</Ellipse>
</Canvas>
</DataTemplate>
add a ValuePresenter
for editing shape's properties:
<Border Grid.Column="1"
BorderBrush="Blue"
BorderThickness="2"
Background="LightBlue"
CornerRadius="5"
Margin="5" Padding="5">
<ObjectPresentation:ValuePresenter x:Name="vp"
ValueType="{x:Type local:MyShape}" />
</Border>
and, add a ContentControl
for presenting the ValuePresenter
's value (the shape):
<ContentControl Name="myContent"
Content="{Binding ElementName=vp, Path=Value, Mode=TwoWay}" />
For presenting colors, we create a UserControl
:
<UserControl x:Class="ObjectPresentation.Examples.Client.ColorPicker"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="R:" VerticalAlignment="Center" />
<Slider Name="rVal" Minimum="0" Maximum="255" Value="60"
Grid.Column="1" Margin="0,2" ValueChanged="OnValueChanged" />
<TextBlock Text="G:" Grid.Row="1" VerticalAlignment="Center"/>
<Slider Name="gVal" Minimum="0" Maximum="255" Value="60"
Grid.Column="1" Grid.Row="1" Margin="0,2"
ValueChanged="OnValueChanged" />
<TextBlock Text="B:" Grid.Row="2" VerticalAlignment="Center" />
<Slider Name="bVal" Minimum="0" Maximum="255" Value="60"
Grid.Column="1" Grid.Row="2" Margin="0,2"
ValueChanged="OnValueChanged" />
<TextBlock Text="A:" Grid.Row="3" VerticalAlignment="Center" />
<Slider Name="aVal" Minimum="0" Maximum="255" Value="60"
Grid.Column="1" Grid.Row="3" Margin="0,2"
ValueChanged="OnValueChanged" />
<Grid Grid.Column="2" Grid.RowSpan="4" Margin="5" >
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Rectangle>
<Rectangle.Fill>
<LinearGradientBrush SpreadMethod="Repeat" StartPoint="0.5,0"
EndPoint="0.5,0.2" >
<GradientStop Color="White" Offset="0" />
<GradientStop Color="White" Offset="0.5" />
<GradientStop Color="LightGray" Offset="0.5" />
<GradientStop Color="LightGray" Offset="1" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Grid.Column="1">
<Rectangle.Fill>
<LinearGradientBrush SpreadMethod="Repeat" StartPoint="0.5,0"
EndPoint="0.5,0.2" >
<GradientStop Color="LightGray" Offset="0" />
<GradientStop Color="LightGray" Offset="0.5" />
<GradientStop Color="White" Offset="0.5" />
<GradientStop Color="White" Offset="1" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Grid.ColumnSpan="2"
Stroke="Black"
StrokeThickness="1"
Width="16" >
<Rectangle.Fill>
<SolidColorBrush Color="{Binding RelativeSource=
{RelativeSource Mode=FindAncestor,
AncestorType={x:Type UserControl}}, Path=SelectedColor}" />
</Rectangle.Fill>
</Rectangle>
</Grid>
</Grid>
</UserControl>
public partial class ColorPicker : UserControl
{
public ColorPicker()
{
InitializeComponent();
rVal.Value = SelectedColor.R;
gVal.Value = SelectedColor.G;
bVal.Value = SelectedColor.B;
aVal.Value = SelectedColor.A;
}
#region SelectedColor
public Color SelectedColor
{
get { return (Color)GetValue(SelectedColorProperty); }
set { SetValue(SelectedColorProperty, value); }
}
public static readonly DependencyProperty SelectedColorProperty =
DependencyProperty.Register("SelectedColor", typeof(Color),
typeof(ColorPicker), new UIPropertyMetadata(Colors.Transparent,
OnSelectedColorChanged));
private static void OnSelectedColorChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs e)
{
ColorPicker cp = sender as ColorPicker;
if (cp == null)
{
return;
}
Color newColor = cp.SelectedColor;
cp.rVal.Value = newColor.R;
cp.gVal.Value = newColor.G;
cp.bVal.Value = newColor.B;
cp.aVal.Value = newColor.A;
}
#endregion
private void OnValueChanged(object sender, RoutedPropertyChangedEventArgs<double /> e)
{
if (!IsLoaded)
{
return;
}
SelectedColor = new Color
{
R = (byte)rVal.Value,
G = (byte)gVal.Value,
B = (byte)bVal.Value,
A = (byte)aVal.Value
};
}
}
create a data-template that uses this UserControl
for the Color
type:
<Grid.Resources>
<x:ArrayExtension x:Key="typeDataTemplates"
Type="{x:Type ObjectPresentation:TypeDataTemplate}">
<ObjectPresentation:TypeDataTemplate ValueType="{x:Type Color}">
<ObjectPresentation:TypeDataTemplate.ValueViewModelDataTemplate>
<DataTemplate>
<local:ColorPicker Width="150"
SelectedColor="{Binding Path=Value,
Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</ObjectPresentation:TypeDataTemplate.ValueViewModelDataTemplate>
</ObjectPresentation:TypeDataTemplate>
</x:ArrayExtension>
</Grid.Resources>
and, set the data-template in the ValuePresenter
:
<ObjectPresentation:ValuePresenter x:Name="vp"
ValueType="{x:Type local:MyShape}"
DataTemplates="{StaticResource typeDataTemplates}" />
The result can be shown as the following:
Using MethodPresenter
For demonstrating the use of the MethodPresenter
control, we create a window that enables adding shapes to a Canvas
.
For that window, we add a Canvas
for holding the shapes:
<Canvas Name="myCanvas" />
add a MethodPresenter
:
<ObjectPresentation:MethodPresenter Name="myMethodPresenter"
Grid.Column="1"
DataTemplates="{StaticResource typeDataTemplates}"
Margin="5" />
add a method for adding shape (using the shape that we created in the previous example):
public void AddShape(MyShape shape)
{
if (shape == null)
{
return;
}
Dispatcher.BeginInvoke(DispatcherPriority.Normal,
new ThreadStart(() =>
{
Shape element = null;
if (shape is MyCircle)
{
MyCircle mc = shape as MyCircle;
element = new Ellipse
{
Width = mc.Diameter,
Height = mc.Diameter
};
}
else if (shape is MyRectangle)
{
MyRectangle mr = shape as MyRectangle;
element = new Rectangle
{
Width = mr.Width,
Height = mr.Height,
};
}
if (element != null)
{
element.Fill = new SolidColorBrush(shape.FillColor);
element.Stroke = new SolidColorBrush(shape.StrokeColor);
element.StrokeThickness = shape.StrokeThickness;
Canvas.SetLeft(element, shape.Left);
Canvas.SetTop(element, shape.Top);
myCanvas.Children.Add(element);
}
}));
}
and, set the MethodInformation
of the MethodPresenter
to that method:
public MethodPresenterExample()
{
InitializeComponent();
myMethodPresenter.MethodInformation = this.GetType().GetMethod("AddShape");
myMethodPresenter.ObjectInstance = this;
}
The result can be shown as the following:
Using InterfacePresenter
For demonstrating the use of the InterfacePresenter
control, we create a WCF service and a GUI client that calls the service's methods.
For the service, we create some data-contracts:
[DataContract]
public class Person
{
[DataMember]
public string Name { get; set; }
[DataMember]
public int Age { get; set; }
[DataMember]
public PersonGender Gender { get; set; }
[DataMember]
public Address Address { get; set; }
[DataMember]
public List<Person> Children { get; set; }
}
[DataContract]
public enum PersonGender
{
[EnumMember]
Male,
[EnumMember]
Female
}
[DataContract]
public class Address
{
[DataMember]
public string Country { get; set; }
[DataMember]
public string City { get; set; }
[DataMember]
public string Street { get; set; }
[DataMember]
public int Number { get; set; }
}
create a service-contract:
[ServiceContract]
public interface IMyService
{
[OperationContract]
void AddPerson(Person person);
[OperationContract]
Person GetPersonByName(string name);
[OperationContract]
Person[] GetAllPersons();
[OperationContract]
double Add(double a, double b);
[OperationContract]
double Sub(double a, double b);
[OperationContract]
double Mul(double a, double b);
[OperationContract]
double Div(double a, double b);
}
create a class that implements the service-contract:
public class MyService : IMyService
{
private List<Person> _persons;
public MyService()
{
_persons = new List<Person>();
}
public void AddPerson(Person person)
{
if (person != null)
{
_persons.Add(person);
}
}
public Person GetPersonByName(string name)
{
return _persons.FirstOrDefault(p => p.Name == name);
}
public Person[] GetAllPersons()
{
return _persons.ToArray();
}
public double Add(double a, double b)
{
return a + b;
}
public double Sub(double a, double b)
{
return a - b;
}
public double Mul(double a, double b)
{
return a * b;
}
public double Div(double a, double b)
{
return a / b;
}
}
add configuration for the service:
<system.serviceModel>
<services>
<service name="ObjectPresentation.Examples.Service.MyService">
<endpoint address="net.tcp://localhost:8123/MyService"
binding="netTcpBinding"
contract="ObjectPresentation.Examples.Contracts.IMyService" />
</service>
</services>
</system.serviceModel>
and, create a ServiceHost
using the service's class:
ServiceHost host = new ServiceHost(typeof(MyService));
host.Open();
Console.WriteLine("Press <Enter> to stop.");
Console.ReadLine();
host.Close();
For showing the inputs that arrive to the service's methods and the results, we can use my LoggingBehavior as the following:
[LoggingBehavior]
public class MyService : IMyService
{
...
}
For the client, we add configuration for the service:
<system.serviceModel>
<client>
<endpoint address="net.tcp://localhost:8123/MyService"
binding="netTcpBinding"
contract="ObjectPresentation.Examples.Contracts.IMyService"
name="myServiceEp" />
</client>
</system.serviceModel>
add an InterfacePresenter
:
<ObjectPresentation:InterfacePresenter x:Name="myInterfacePresenter" />
and, set its ObjectInstance
to the service's proxy:
public InterfacePresenterExample()
{
InitializeComponent();
ChannelFactory<IMyService> factory = new ChannelFactory<IMyService>("myServiceEp");
IMyService proxy = factory.CreateChannel();
myInterfacePresenter.InterfaceType = typeof(IMyService);
myInterfacePresenter.ObjectInstance = proxy;
}
The result can be shown as the following:
Using ObjectPresenter
For demonstrating the use of the ObjectPresenter
control, we add some methods to the Person
class:
[DataContract]
public class Person
{
[DataMember]
public string Name { get; set; }
[DataMember]
public int Age { get; set; }
[DataMember]
public PersonGender Gender { get; set; }
[DataMember]
public Address Address { get; set; }
[DataMember]
public List<Person> Children { get; set; }
public string[] GetChildrenNames()
{
if (Children == null)
{
return null;
}
return Children.Select(c => c.Name).ToArray();
}
public Person GetChildByName(string name)
{
return Children.FirstOrDefault(c => c.Name == name);
}
public void AddChild(Person child)
{
if (Children == null)
{
Children = new List<Person>();
}
Children.Add(child);
}
public void SetAddress(Address address)
{
this.Address = address;
}
}
add an ObjectPresenter
:
<ObjectPresentation:ObjectPresenter Name="myObjectPresenter" />
and, set its ObjectInstance
to a Person
object:
public ObjectPresenterExample()
{
InitializeComponent();
myObjectPresenter.ObjectInstance = new Person();
}
The result can be shown as the following: