65.9K
CodeProject 正在变化。 阅读更多。
Home

扩展 ObjectPresenter

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2018年3月19日

CPOL

8分钟阅读

viewsIcon

9340

downloadIcon

190

本文展示了我们如何扩展 ObjectPresentation 库的行为。

目录

引言

有时,在我们工作中,会发现需要将旧代码用于新的目的。当事情顺利进行时,这可能非常容易。但是,当遇到不兼容或不受支持的问题时,事情可能会变得更加复杂。

在我的情况下,我想使用我旧的 ObjectPresentation 库,来为一些新组件生成测试 GUI。但是,新组件包含 Tuple 对象,这些对象与基本库的行为不兼容。ObjectPresentation 库为所有可以设置的属性生成输入字段。由于 Tuple 属性是只读的,因此没有为它们生成输入字段。

由于我不想让这个小问题阻碍我使用这个库,我决定实现一个扩展来让它正常工作。

ObjectPresentation 库的另一个问题是,任何呈现的引用类型都可以选择折叠或设置为 null。有时,我们只想显示对象的属性,而不提供设置整个对象值(例如在 ValuePresenter 示例 中的情况)的选项。由于我认为这也很常见,所以我决定也为这种情况实现一个扩展。

在本文中,我们将讨论如何用新行为扩展 ObjectPresentation 库。

背景

ObjectPresentation 库提供的扩展点之一是 定义附加数据模板 的能力。我不得不承认,这个扩展点的主要目的是为一些特殊类型(如示例中的 ColorPicker)创建简单的数据模板(使用 XAML)。但是,本文中案例的复杂性,促使我更进一步,并实现了一个更复杂的解决方案。在本文中,我们将展示如何通过代码创建通用数据模板解决方案,以扩展 ObjectPresentation 库的行为。

本文假设您熟悉 C# 语言和 WPF 框架。本文使用了 ObjectPresentation 库,因此建议您熟悉 该库的使用

工作原理

显示 Tuple 属性

从 Tuple 属性获取 Tuple 值

正如我们所提到的,Tuple 属性是只读的。因此,无法通过调用它们的 setter 来设置它们的属性(它们没有 setter)。但是,幸运的是,每种 Tuple 类型都有一个构造函数,它在参数中接受适当的属性值。我们可以为我们的目的使用这个特性。

为了显示 Tuple 属性的输入字段,我们添加了一个控件来处理我们想要的行为。

public class TupleContentControl : ContentControl
{
}

在该控件中,我们添加一个字段来保存 Tuple 属性,并将其设置为控件的 Content

public class TupleContentControl : ContentControl
{
    public TupleContentControl()
    {
        _tvvm = new TupleValueViewModel();

        Content = _tvvm;
    }
}

我们使用一个派生自 ValueViewModel 的类来保存属性,因为我们希望保留原始数据模板的功能(如折叠和展开等),也适用于 Tuple 类型。

为了创建属性输入字段(用于输入数据),我们添加一个字段来保存 Tuple 的类型,并为每个 Tuple 属性(根据 Tuple 的类型)创建一个 PropertyInputValueViewModel

#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);
            }
        }
    }
}

为了设置 Tuple 属性值,我们添加一个字段来保存 Tuple 的值,并在值更改时设置属性输入字段的值。

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
                    });
            }
        }
    }
}

setTupleValue 方法中,我们区分输入数据和输出数据两种情况。对于输入数据,我们只需为每个属性输入字段(已在 createTupleProperties 中创建)设置适当的属性值。对于输出数据,我们创建一个 OutputValueViewModel,并用属性名和值对其进行设置。

为了获取 Tuple 的值,我们注册到每个属性输入字段的 PropertyChanged 事件,并在每个属性更改时创建一个 Tuple 对象实例(使用带有属性值的构造函数)。

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);
    }
}

查找 Tuple 类型

在拥有一个用于显示 Tuple 的控件后,我们可以将其设置为所需 Tuple 类型的数据模板(由于它是一个泛型类,可能会有无限数量的生成类型)。在创建数据模板之前,我们必须知道使用了哪些 Tuple 类型。这可以通过以下方式完成。

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`");
}

getTupleTypes 方法中,我们获取一个 Type,遍历其所有属性、字段、方法的参数和返回值,并获取 Tuple 类型家族中使用的类型。我们还对内部类型递归地执行相同的过程,以涵盖它们的 Tuple 类型。

创建 Tuple 数据模板

使用我们的控件和获取到的 Tuple 类型,我们可以创建我们的数据模板扩展。这可以通过以下方式完成。

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;
}

GetTupleTypeDataTemplates 方法中,对于每个 Tuple 类型,我们创建一个带有我们的 TupleContentControl 控件的 FrameworkElementFactory 实例。在该实例上,我们将控件的 TupleType 属性绑定到 Tuple 的类型,并将控件的 TupleValue 属性绑定到 DataContext(即模板化的 ValueViewModel)的 Value 属性。使用创建的 FrameworkElementFactory,我们创建一个 DataTemplate,然后创建一个带有创建的数据模板和 Tuple 类型的 TypeDataTemplate 对象。

为了从 XAML 代码中使用此实用程序,我们添加了一个适当的转换器。

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();
    }
}

显示固定对象属性

下一个挑战是显示对象的属性,而不能折叠它们或将对象的值设置为 null。与 Tuple 情况一样,为了实现此行为,我们使用一个辅助控件。

public class FixedPropertiesInputItemsControl : ItemsControl
{
}

在该控件中,为了移除原始数据模板的行为(允许折叠和设置为 null),我们将原始 SubFields 存储在一个单独的字段中,并清除原始 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" });
    }
}

onInitialized 方法中,我们将原始 SubFields 复制到一个单独的字段,并将复制的 SubFields 设置为控件的 ItemsSource 属性。

clearValueViewModel 方法中,我们清除原始 SubFields,将 private_selectedCompatibleType 字段设置为 null(此字段会影响 IsNullable 属性的值,该值决定是否显示“设为 null”按钮),并通知 IsNullable 属性已更改。此实现有点棘手。它使用了 private 数据并假定了内部实现。但是,由于我认为我不会更改该实现,因此可以接受。

为了获取对象的值,我们注册到每个子字段的 PropertyChanged 事件,并在每个属性更改时,将原始 ValueViewModel 的值设置为新值。

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);
        }
    }
}

我们使用派生 InputValueViewModelGenerateValueFromSubFields 方法,从子字段构建新值。

使用辅助控件,我们创建一个 TypeDataTemplate 扩展(与我们为 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))
        }
    };
}

为了从 XAML 代码中使用此实用程序,我们添加了一个适当的转换器。

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();
    }
}

为了使用一些 TypeDataTemplate 扩展,我们添加了一个用于合并 TypeDataTemplate 集合的转换器。

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;
    }
}

如何使用

使用 Tuple 扩展

为了演示 Tuple 扩展的用法,我们添加了一个包含 2 个使用 Tuple 类型的方法的接口。

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);
}

以及一个实现该接口的类。

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;
    }
}

Div 方法中,我们获取一个包含两个操作数的 Tuple 参数,并返回一个包含除法结果和余数的 Tuple

DoOperations 方法中,我们获取一个包含内部 Tuple 属性和一个 Tuple 集合的 Tuple 参数。每个内部 Tuple 包含操作数和运算符。返回值是一个包含操作结果的 Tuple

为了显示操作 GUI,我们添加了一个包含操作对象和接口类型的属性的视图模型。

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; }
}

以及一个显示这些属性的 InterfacePresenter 控件。

<objectPresentation:InterfacePresenter ObjectInstance = "{Binding OperationsObject}"
                                        InterfaceType="{Binding OperationsInterfaceType}" />

为了使 Tuple 类型能够工作,我们使用 Tuple 扩展来添加(对于所呈现对象中每个 Tuple 类型所需的)Tuple 数据模板。

<Window.Resources>
    <objectPresentationConverters:TupleTypesDataTemplatesFromTypeConverter 
     x:Key="TupleTypesDataTemplatesFromTypeConverter" />
</Window.Resources>

...

    <objectPresentation:InterfacePresenter ObjectInstance = "{Binding OperationsObject}"
                                            InterfaceType="{Binding OperationsInterfaceType}"
        DataTemplates="{Binding OperationsInterfaceType, 
        Converter={StaticResource TupleTypesDataTemplatesFromTypeConverter}}" />

结果是:

Tuple extension example

使用固定属性输入扩展

为了演示固定属性输入扩展的用法,我们使用与原始 ValuePresenter 示例 相同的示例,并将其改进为作为固定属性视图进行呈现。

为了显示固定属性视图,我们为形状类型(除了现有的 Color 数据模板之外)添加了“固定属性”数据模板。

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

我们还添加了一个默认形状值作为初始呈现的形状。

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>

结果是:

Fixed Proprties extension example

结论

在本文中,我们创建了两个 TypeDataTample 扩展,并使用它们来改进 ObjectPresentation 库控件的行为。

在第一个扩展中,我们创建了一个解决方案来显示使用 Tuple 类型的组件。为此,我们创建了一个实用程序,用于为给定类型中使用的每个 Tuple 类型创建可以显示 Tuple 类型的数据模板。

在第二个扩展中,我们创建了一个解决方案来移除折叠属性视图或将对象设为 null 的能力。这可以用于在需要时显示固定属性视图。

目前,我们只有这两个扩展。但是,如果发现合适的需要,可能会向本文添加更多扩展。所以,如果您对有价值的扩展有什么想法,请随时在下方留言。

© . All rights reserved.