Click here to Skip to main content
15,884,388 members
Articles / Desktop Programming / WPF

Extending ObjectPresenter

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
19 Mar 2018CPOL8 min read 8.5K   126   5   2
This article shows how we can extend the ObjectPresentation library behavior.

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:

C#
public class TupleContentControl : ContentControl
{
}

In that control, we add a field for holding the Tuple properties and, set it as the control's Content:

C#
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):

C#
#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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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):

C#
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:

C#
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:

C#
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:

C#
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:

C#
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 Tuples. 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:

C#
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:

XML
<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:

XML
<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:

Tuple extension example

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):

XML
<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:

C#
public class ExampleViewModel : BaseViewModel
{
    // ...

    public MyShape InitialShape { get; set; }
}
XML
<objectPresentation:ValuePresenter x:Name="vp"
                                    ValueType="{x:Type local:MyShape}"
                                    Value="{Binding InitialShape, Mode=OneTime}">

        ...

</objectPresentation:ValuePresenter>

The result is:

Fixed Proprties extension example

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Israel Israel
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
PraiseMy Pin
jediYL20-Mar-18 15:18
professionaljediYL20-Mar-18 15:18 
GeneralRe: My Pin
Shmuel Zang2-Apr-18 22:53
Shmuel Zang2-Apr-18 22:53 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.