Table of Contents
Introduction
Sometimes, during our work, we find a need of reusing an old code for new purposes. It can be very easy when things go smoothly. But, when we have some incompatibilities or unsupported issues, things can be more complicated.
In my case, I wanted to use my old ObjectPresentation library, for generating a testing GUI for some new components. But, the new components contained Tuple objects that are incompatible with the basic library's bahavior. The ObjectPresentation
library generates input fields for all the properties that can be set. Since the Tuple
properties are read-only, no input fields were generated for them.
Since I didn't want that little problem to prevent me from using the library, I decided to implement an extension that make it works.
Another issue about the ObjectPresentation
library is that any reference type that is presented has the option to be collapsed or set to null
. Sometimes, we just want to present the object's properties without giving the option for setting the entire object's value (like the case in the ValuePresenter example). Since I think that it's a common need too, I decided to implement an extension also for that case.
In this article, we discuss about how we can extend the ObjectPresentation
library with new behaviors.
Background
One of the extension points that we have in the ObjectPresentation
library is the ability of defining additional data-templates. I have to admit that the main purpose of that extension point was for creating simple data-templates (using XAML) for some special types (like the ColorPicker
in the examples). But, the complexity of the cases in this article, made me to take it one step further and, implememt a little more complicated solution. In this article, we show how we can create generic data-templates (using code) solutions, for extending the ObjectPresentation
library's bahavior.
This article assumes a familiarity with the C# language and the WPF framework. This article uses the ObjectPresentation
library, so a familiarity with the use of that library is recommended too.
How It Works
Present Tuple Properties
Get Tuple Value from Tuple Properties
As we mentioned, the Tuple
properties are read-only. So, there is no way for setting its properties by calling their setters (there are no setters for them). But, furtunately, each Tuple
type has a constructor that takes the appropriate properties values in its arguments. We can use that feature for our purpose.
For presenting input fields for the Tuple's properties, we add a control that handles our wanted behavior:
public class TupleContentControl : ContentControl
{
}
In that control, we add a field for holding the Tuple
properties and, set it as the control's Content
:
public class TupleContentControl : ContentControl
{
public TupleContentControl()
{
_tvvm = new TupleValueViewModel();
Content = _tvvm;
}
}
We use a ValueViewModel
derived class for holding the properties, since we want to keep the original data-template features (like collapsing and expanding, etc.), for the Tuple
type too.
For creating the properties input fields (for an input data), we add a property for holding the Tuple's type and, create a PropertyInputValueViewModel
for each Tuple property (according to the Tuple
's type):
#region TupleType
public Type TupleType
{
get { return (Type)GetValue(TupleTypeProperty); }
set { SetValue(TupleTypeProperty, value); }
}
public static readonly DependencyProperty TupleTypeProperty =
DependencyProperty.Register("TupleType", typeof(Type), typeof(TupleContentControl),
new PropertyMetadata(null, new PropertyChangedCallback(onTupleTypeChanged)));
private static void onTupleTypeChanged(DependencyObject o, DependencyPropertyChangedEventArgs arg)
{
TupleContentControl tcc = o as TupleContentControl;
if (tcc != null)
{
tcc.createTupleProperties();
}
}
#endregion
private void createTupleProperties()
{
if (TupleType != null)
{
ValueViewModel vvm = DataContext as ValueViewModel;
if (vvm == null || vvm.IsEditable)
{
_tvvm.SubFields.Clear();
foreach (var prop in TupleType.GetProperties())
{
var newProp = new PropertyInputValueViewModel(prop)
{
DataTemplates = vvm != null ? vvm.DataTemplates : null,
AutoGenerateCompatibleTypes = vvm != null ? vvm.AutoGenerateCompatibleTypes : false,
KnownTypes = vvm != null ? vvm.KnownTypes : null
};
_tvvm.SubFields.Add(newProp);
}
}
}
}
For setting the Tuple
properties values, we add a property for holding the Tuple
's value and, set the properties input fields values, when the value is changed:
public class TuplePropertyOutputValueViewModel : OutputValueViewModel
{
public TuplePropertyOutputValueViewModel(string name, object value, int valueLevel = 0)
: base(value, valueLevel)
{
Name = name;
}
}
#region TupleValue
public object TupleValue
{
get { return (object)GetValue(TupleValueProperty); }
set { SetValue(TupleValueProperty, value); }
}
public static readonly DependencyProperty TupleValueProperty =
DependencyProperty.Register("TupleValue", typeof(object),
typeof(TupleContentControl), new PropertyMetadata(null,
new PropertyChangedCallback(onTupleValueChanged)));
private static void onTupleValueChanged(DependencyObject o, DependencyPropertyChangedEventArgs arg)
{
TupleContentControl tcc = o as TupleContentControl;
if (tcc != null)
{
tcc.setTupleValue();
}
}
#endregion
void setTupleValue()
{
if (TupleValue != null &&
TupleValue.GetType() == TupleType && IsTupleType(TupleType))
{
PropertyInfo[] tupleProps = TupleType.GetProperties();
ValueViewModel vvm = DataContext as ValueViewModel;
if (vvm == null || vvm.IsEditable)
{
if (_tvvm.SubFields.Count == tupleProps.Length)
{
int propInx = 0;
foreach (var prop in _tvvm.SubFields)
{
prop.Value = tupleProps[propInx].GetValue(TupleValue);
propInx++;
}
}
}
else
{
_tvvm.IsExpandedByDefault = true;
_tvvm.SubFields.Clear();
foreach (PropertyInfo prop in tupleProps)
{
_tvvm.SubFields.Add(
new TuplePropertyOutputValueViewModel(prop.Name, prop.GetValue(TupleValue))
{
DataTemplates = vvm.DataTemplates,
AutoGenerateCompatibleTypes = vvm.AutoGenerateCompatibleTypes,
KnownTypes = vvm.KnownTypes
});
}
}
}
}
In the setTupleValue
method, we distinguish between the two cases of input or output data. For input data, we just set each property input (already created in createTupleProperties
) field with the appropriate property value. For output data, we create an OutputValueViewModel
and set it with the property's name and value.
For getting the Tuple
's value, we register to the PropertyChanged
event of each property input field and, create a Tuple
object instance (using the constructor with the properties' values) when each property is changed:
private void createTupleProperties()
{
if (TupleType != null)
{
ValueViewModel vvm = DataContext as ValueViewModel;
if (vvm == null || vvm.IsEditable)
{
foreach (ValueViewModel oldProp in _tvvm.SubFields)
{
oldProp.PropertyChanged -= onTuplePropertyChanged;
}
_tvvm.SubFields.Clear();
foreach (var prop in TupleType.GetProperties())
{
var newProp = new PropertyInputValueViewModel(prop)
{
DataTemplates = vvm != null ? vvm.DataTemplates : null,
AutoGenerateCompatibleTypes = vvm != null ? vvm.AutoGenerateCompatibleTypes : false,
KnownTypes = vvm != null ? vvm.KnownTypes : null
};
_tvvm.SubFields.Add(newProp);
newProp.PropertyChanged += onTuplePropertyChanged;
}
}
}
}
private void onTuplePropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Value")
{
updateTupleValue();
}
}
private void updateTupleValue()
{
if (IsTupleType(TupleType) && _tvvm.HasSubFields)
{
object[] ctorParams = _tvvm.SubFields.Select(p => p.Value).ToArray();
TupleValue = Activator.CreateInstance(TupleType, ctorParams);
}
}
Find Tuple Types
After we have a control for presenting a Tuple
, we can set it as a data-template for the needed Tuple
types (Since it is a generic class, there can be an infinite number of generated types). Before we create the data-templates, we have to know what are the used Tuple types. That can be done as follows:
private static IEnumerable<Type> getTupleTypes(Type objectType, List<Type> usedTypes = null)
{
bool isRootType = false;
if (usedTypes == null)
{
usedTypes = new List<Type>();
isRootType = true;
}
if (usedTypes.Contains(objectType))
{
return _emptyTypes;
}
usedTypes.Add(objectType);
IEnumerable<Type> tupleTypes = objectType.GetProperties().Select(p => p.PropertyType);
tupleTypes = tupleTypes.Concat(objectType.GetFields().Select(f => f.FieldType));
tupleTypes = tupleTypes.Concat(objectType.GetMethods().SelectMany
(m => m.GetParameters().Select(p => p.ParameterType)));
tupleTypes = tupleTypes.Concat(objectType.GetMethods().Select(m => m.ReturnType));
IEnumerable<Type> subTypes = tupleTypes.SelectMany(t => getTupleTypes(t, usedTypes));
tupleTypes = tupleTypes.Concat(subTypes);
if (isRootType && IsTupleType(objectType))
{
tupleTypes.Concat(new Type[1] { objectType });
}
tupleTypes = tupleTypes.Where(t => IsTupleType(t)).Distinct();
return tupleTypes;
}
public static bool IsTupleType(Type t)
{
return t != null && t.IsGenericType && t.Name.StartsWith("Tuple`");
}
In the getTupleTypes
method, we get a Type
and go over all its properties, fields, methods' parameters and return values, and get the used types that are in the Tuple
types family. We do the same process also for the inner types recursively, in order to cover their Tuple
types too.
Create Tuple data-templates
Using our control and, the gotten Tuple
types, we can create our data-templates extensions. That can be done as follows:
public static TypeDataTemplate[] GetTupleTypeDataTemplates(Type objectType)
{
if (objectType == null)
{
return null;
}
IEnumerable<Type> tupleTypes = getTupleTypes(objectType);
TypeDataTemplate[] res = tupleTypes.Select(t =>
{
FrameworkElementFactory controlFactory =
new FrameworkElementFactory(typeof(TupleContentControl));
controlFactory.SetBinding(TupleContentControl.TupleTypeProperty, new Binding
{
Source = t
});
controlFactory.SetBinding(TupleContentControl.TupleValueProperty, new Binding
{
Path = new PropertyPath("Value"),
Mode = BindingMode.TwoWay,
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
});
return new TypeDataTemplate
{
ValueType = t,
ValueViewModelDataTemplate = new DataTemplate
{
VisualTree = controlFactory
}
};
}).ToArray();
return res;
}
In the GetTupleTypeDataTemplates
method, for each Tuple
type, we create a FrameworkElementFactory
instance with our TupleContentControl
control. On that instance, we bind the control's TupleType
property to the Tuple
's type and, bind the control's TupleValue
property to the DataContext
's (the templated ValueViewModel
) Value
property. Using the created FrameworkElementFactory
, we create DataTemplate
and create a TypeDataTemplate
object with the created data-template and the Tuple
's type.
For using this utility from XAML code, we add an appropriate converter:
public class TupleTypesDataTemplatesFromTypeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return Utils.GetTupleTypeDataTemplates(value as Type);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new InvalidOperationException();
}
}
Present Fixed Object Properties
The next challenge is presenting an object's properties without the ability of collapsing them or setting the object's value to null
. Like with the Tuple
case, for achieving that behavior, we use a helper control:
public class FixedPropertiesInputItemsControl : ItemsControl
{
}
In that control, in order to remove the original data-template's behavior (allowing collapsing and setting null
), we store the original SubFields
in a separate field and, clear the data from the original ValueViewModel
:
public class FixedPropertiesInputValueViewModel : InputValueViewModel
{
}
public class FixedPropertiesInputItemsControl : ItemsControl
{
private FixedPropertiesInputValueViewModel _fpivvm;
public FixedPropertiesInputItemsControl()
{
_fpivvm = new FixedPropertiesInputValueViewModel();
Initialized += onInitialized;
}
private void onInitialized(object sender, EventArgs e)
{
ValueViewModel vvm = DataContext as ValueViewModel;
if (vvm != null)
{
copyValueViewModel(vvm);
clearValueViewModel(vvm);
SetBinding(ItemsControl.ItemsSourceProperty,
new Binding { Source = _fpivvm, Path = new PropertyPath("SubFields") });
}
}
private void copyValueViewModel(ValueViewModel vvm)
{
_fpivvm.ValueType = vvm.ValueType;
_fpivvm.SelectedCompatibleType = vvm.SelectedCompatibleType;
_fpivvm.SubFields.Clear();
foreach (var subField in vvm.SubFields)
{
_fpivvm.SubFields.Add(subField);
}
}
private void clearValueViewModel(ValueViewModel vvm)
{
vvm.SubFields.Clear();
Type vvmType = typeof(ValueViewModel);
FieldInfo fi = vvmType.GetField("_selectedCompatibleType",
BindingFlags.NonPublic | BindingFlags.Instance);
fi?.SetValue(vvm, null);
MethodInfo mi = vvmType.GetMethod("NotifyPropertyChanged",
BindingFlags.NonPublic | BindingFlags.Instance);
mi?.Invoke(vvm, new object[] { "IsNullable" });
}
}
In the onInitialized
method, we copy original SubFields
to a separate field and, set the copied SubFields
as the ItemsSource
property of the control.
In the clearValueViewModel
method, we clear the original SubFields
, set the private
_selectedCompatibleType
fields to null
(this field affects the IsNullable
property's value which determines if the "make null
" button is presented) and, notify about the IsNullable
property change. This implementation is a little tricky. It uses a private
data and takes assumptions about an internal implementation. But, since I don't think I'm going to change that implementation, it can be acceptable.
For getting the object's value, we register to the PropertyChanged
event of each sub-field and, set the original ValueViewModel
's value with the new value, for each property change:
public class FixedPropertiesInputValueViewModel : InputValueViewModel
{
protected override object GetValue()
{
if (HasSubFields)
{
return GenerateValueFromSubFields();
}
return _value;
}
}
private void copyValueViewModel(ValueViewModel vvm)
{
_fpivvm.ValueType = vvm.ValueType;
_fpivvm.SelectedCompatibleType = vvm.SelectedCompatibleType;
_fpivvm.SubFields.Clear();
foreach (var subField in vvm.SubFields)
{
subField.PropertyChanged += onSubPropertyChanged;
_fpivvm.SubFields.Add(subField);
}
}
private void onSubPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Value")
{
ValueViewModel vvm = DataContext as ValueViewModel;
if (vvm != null)
{
vvm.Value = _fpivvm.Value;
clearValueViewModel(vvm);
}
}
}
We build the new value form the sub-fields using the GenerateValueFromSubFields
method of the derived InputValueViewModel
.
Using the helper control, we create a TypeDataTemplate
extension (in the same manner as we did for the Tuple
):
public static TypeDataTemplate GetFixedPropertiesInputTypeDataTemplate(Type objectType)
{
if (objectType == null)
{
return null;
}
return new TypeDataTemplate
{
ValueType = objectType,
ValueViewModelDataTemplate = new DataTemplate
{
VisualTree = new FrameworkElementFactory(typeof(FixedPropertiesInputItemsControl))
}
};
}
For using this utility from XAML code, we add an appropriate converter:
public class FixedPropertiesInputTypeDataTemplateFromTypeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return Utils.GetFixedPropertiesInputTypeDataTemplate(value as Type);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new InvalidOperationException();
}
}
In order to use some TypeDataTemplate
extensions, we add a converter for merging TypeDataTemplate
collections:
public class TypeDataTemplateCollectionConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return mergeValues(values).ToArray();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new InvalidOperationException();
}
private IEnumerable<TypeDataTemplate> mergeValues(object[] values)
{
if (values != null)
{
foreach (object value in values)
{
if (value is TypeDataTemplate)
{
yield return value as TypeDataTemplate;
}
else if (value is IEnumerable<TypeDataTemplate>)
{
IEnumerable<TypeDataTemplate> collection = value as IEnumerable<TypeDataTemplate>;
foreach (TypeDataTemplate elem in collection)
{
yield return elem;
}
}
}
}
yield break;
}
}
How To Use It
Using Tuple Extension
For demonstrating the use of the Tuple
extension, we add an interface with 2 methods that use Tuple
types:
public enum OperationOperator
{
ADD,
SUB,
MUL,
DIV
}
public interface IMyOperations
{
Tuple<int, int> Div(Tuple<int, int> numbers);
Tuple<double, double, double[]> DoOperations(Tuple<Tuple<double, double, OperationOperator>,
Tuple<double, double, OperationOperator>, Tuple<double, double, OperationOperator>[]> operations);
}
and, a class that implements that interface:
public class MyOperations : IMyOperations
{
public Tuple<int, int> Div(Tuple<int, int> numbers)
{
int result = numbers.Item1 / numbers.Item2;
int reminder = numbers.Item1 % numbers.Item2;
return new Tuple<int, int>(result, reminder);
}
public Tuple<double, double, double[]> DoOperations
(Tuple<Tuple<double, double, OperationOperator>, Tuple<double, double, OperationOperator>,
Tuple<double, double, OperationOperator>[]> operations)
{
double[] arrayRes = null;
if (operations.Item3 != null)
{
arrayRes = new double[operations.Item3.Length];
for (int operInx = 0; operInx < operations.Item3.Length; operInx++)
{
arrayRes[operInx] = doOperation(operations.Item3[operInx]);
}
}
return new Tuple<double, double, double[]>(
doOperation(operations.Item1), doOperation(operations.Item2), arrayRes);
}
private double doOperation(Tuple<double, double, OperationOperator> operation)
{
if (operation == null)
{
return 0;
}
switch (operation.Item3)
{
case OperationOperator.ADD:
return operation.Item1 + operation.Item2;
case OperationOperator.SUB:
return operation.Item1 - operation.Item2;
case OperationOperator.MUL:
return operation.Item1 * operation.Item2;
case OperationOperator.DIV:
return operation.Item1 / operation.Item2;
}
return 0;
}
}
In the Div
method, we get a Tuple
parameter that contains two operands and, return a Tuple
that contains the result and the reminder of the division operation.
In the DoOperations
method, we get a Tuple
parameter that has inner Tuple
properties and a collection of Tuple
s. Each inner Tuple
contains the operation's operands and the operator. The return value is a Tuple
of the operations' results.
For presenting the operations GUI, we add a view-model that contains properties for the operations object and interface's type:
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
}
public class ExampleViewModel : BaseViewModel
{
public ExampleViewModel()
{
OperationsObject = new MyOperations();
OperationsInterfaceType = typeof(IMyOperations);
}
public MyOperations OperationsObject { get; set; }
public Type OperationsInterfaceType { get; set; }
}
and, an InterfacePresenter
control that presents those properties:
<objectPresentation:InterfacePresenter ObjectInstance = "{Binding OperationsObject}"
InterfaceType="{Binding OperationsInterfaceType}" />
In order to make the Tuple
types work, we use the Tuple
extension for adding the needed (for each Tuple
type in the presented object) Tuple
data-templates:
<Window.Resources>
<objectPresentationConverters:TupleTypesDataTemplatesFromTypeConverter
x:Key="TupleTypesDataTemplatesFromTypeConverter" />
</Window.Resources>
...
<objectPresentation:InterfacePresenter ObjectInstance = "{Binding OperationsObject}"
InterfaceType="{Binding OperationsInterfaceType}"
DataTemplates="{Binding OperationsInterfaceType,
Converter={StaticResource TupleTypesDataTemplatesFromTypeConverter}}" />
The result is:
Using Fixed Properties Input Extension
For demonstrating the use of the Fixed Properties Input extension, we use the same original ValuePresenter example and, improve it to be presented as a fixed properties view.
In order to present a fixed properties view, we add "fixed properties" data templates for the shape's types (in addition to the existing Color
data-template):
<objectPresentation:ValuePresenter x:Name="vp"
ValueType="{x:Type local:MyShape}">
<objectPresentation:ValuePresenter.DataTemplates>
<MultiBinding Converter = "{StaticResource TypeDataTemplateCollectionConverter}" >
<Binding Source="{StaticResource typeDataTemplates}" />
<Binding Source = "{x:Type local:MyShape}"
Converter="{StaticResource FixedPropertiesInputTypeDataTemplateFromTypeConverter}" />
<Binding Source = "{x:Type local:MyRectangle}"
Converter="{StaticResource FixedPropertiesInputTypeDataTemplateFromTypeConverter}" />
<Binding Source = "{x:Type local:MyCircle}"
Converter="{StaticResource FixedPropertiesInputTypeDataTemplateFromTypeConverter}" />
</MultiBinding>
</objectPresentation:ValuePresenter.DataTemplates>
</objectPresentation:ValuePresenter>
We also add a default shape value as the initial presented shape:
public class ExampleViewModel : BaseViewModel
{
public MyShape InitialShape { get; set; }
}
<objectPresentation:ValuePresenter x:Name="vp"
ValueType="{x:Type local:MyShape}"
Value="{Binding InitialShape, Mode=OneTime}">
...
</objectPresentation:ValuePresenter>
The result is:
Conclusion
In this article, we made two TypeDataTample
extensions and used them for improving the behavior of the ObjectPresentation
library controls.
In the first extension, we made a solution for presenting components that use Tuple
types. For that purpose, we made a utility for creating a data-template that can present a Tuple
type, for each used Tuple
type in a given type.
In the second extension, we made a solution for removing the ability of collapsing the properties' view or making the object to be null
. That can be used for presenting a fixed properties view, when it is needed.
For now, we have only these two extensions. But, there can be more extensions that will be added to this article, if we'll find an appropriate need. So, if you have any idea about something that worth an extension, feel free to leave a comment below.