24

I'm working with DevExpress's WPF tree list view and I came across what I think is a more general problem relating to renaming properties on the objects used as an item source. In the tree list view one is required to specify the ParentFieldName and the KeyFieldName (which are used determine the structure of the tree). These fields are strings.

This has led to issues refactoring the code. For example renaming a property of the objects I am using as an ItemSource will break the tree view as ParentFieldName and KeyFieldName are no longer in sync with the property names. I have worked around this issue by creating properties in my view model "ParentFieldName" and "KeyFieldName" which use nameof to present the property name to the view.

Here is a cut down version of the control:

    <UserControl
             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"
             xmlns:dxg="http://schemas.devexpress.com/winfx/2008/xaml/grid"              
             d:DesignHeight="300" d:DesignWidth="300">
      <UserControl.DataContext>
          <ViewModel />
      </UserControl.DataContext>
        <dxg:TreeListControl AutoGenerateColumns="AddNew"
                         EnableSmartColumnsGeneration="True" ItemsSource="{Binding Results}"                        
                         SelectionMode="Row">
            <dxg:TreeListControl.View>
                <dxg:TreeListView                                   
                              ParentFieldName="{Binding ParentIdFieldName}" KeyFieldName="{Binding NodeIdFieldName}" 
                              ShowHorizontalLines="False" ShowVerticalLines="False"
                              ShowNodeImages="True"/>
            </dxg:TreeListControl.View>
        </dxg:TreeListControl>
    </UserControl>

And the viewmodel:

using DevExpress.Mvvm;    

public sealed class ViewModel : ViewModelBase
{
    public string ParentIdFieldName => nameof(TreeNode.ParentId);

    public string NodeIdFieldName => nameof(TreeNode.NodeId);

    public ObservableCollection<TreeNode> Results
    {
        get => GetProperty(() => Results);
        set => SetProperty(() => Results, value);
    } 
}

And the tree node:

public sealed class TreeNode
{
    public int ParentId {get; set;}
    public int NodeId {get; set;}
}

My solution works well but I was wondering if there was a better way of doing this. For example, is there something I can do in XAML which would be equivalent to the nameof call, rather than binding to this ParentIdFieldName and NodeIdFieldName in the view model?

I realize this could be described as an issue with DevExpress's control. However I'm interested in whether the approach I've used to get around this can be improved on. Is there a way I could do this in a more simple way directly in the XAML?

I apologize in advance if the code I've provided doesn't compile. I've cut down what I'm working with quite considerably to provide an example.

luxun
  • 457
  • 5
  • 14

2 Answers2

28

You can create a custom markup extension.

For example:

[ContentProperty(nameof(Member))]
public class NameOfExtension : MarkupExtension
{
    public Type Type { get; set; }
    public string Member { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        if (serviceProvider == null)
            throw new ArgumentNullException(nameof(serviceProvider));

        if (Type == null || string.IsNullOrEmpty(Member) || Member.Contains("."))
            throw new ArgumentException("Syntax for x:NameOf is Type={x:Type [className]} Member=[propertyName]");

        var pinfo = Type.GetRuntimeProperties().FirstOrDefault(pi => pi.Name == Member);
        var finfo = Type.GetRuntimeFields().FirstOrDefault(fi => fi.Name == Member);
        if (pinfo == null && finfo == null)
            throw new ArgumentException($"No property or field found for {Member} in {Type}");

        return Member;
    }
}

Sample usage:

enter image description here

Sharada Gururaj
  • 13,471
  • 1
  • 22
  • 50
  • 1
    Resharper complains about unresolved symbol for Member, but passing it as a constructor parameter fixes it. – Shahin Dohan Sep 25 '18 at 14:19
  • Could this extension be used in bindings, e.g. as Dictionary key? How would the syntax look like? – wondra Jun 09 '20 at 08:54
  • Has anyone found a way to use this for an x:Key directive? I get the following error: `A key for a dictionary cannot be of type 'MyNamespace.NameOfExtension'. Only String, TypeExtension, and StaticExtension are supported.` – DMX David Cardinal Dec 30 '20 at 00:37
  • To use this in a binding, do not use `Path=`, eg: `{Binding {local:NameOf Type={x:Type local:MainWindow}, Member=Title}}` – Maxence Jan 27 '21 at 16:33
11

For some reason my designer isn't working well with G.Sharada's solution, though it works great in runtime.

I went a slightly different route:

public class NameOfExtension : MarkupExtension
{
    private readonly PropertyPath _propertyPath;

    public NameOfExtension(Binding binding)
    {
        _propertyPath = binding.Path;
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        var indexOfLastVariableName = _propertyPath.Path.LastIndexOf('.');
        return _propertyPath.Path.Substring(indexOfLastVariableName + 1);
    }
}

This way I can do:

<TextBlock Text="{local:NameOf {Binding Property1.Property2}}" />

This is not a replacement obviously since we may not have an instantiated object to bind to. I suppose I could have 2 constructors, one which takes a binding, and the other a Member string.

g t
  • 7,287
  • 7
  • 50
  • 85
Shahin Dohan
  • 6,149
  • 3
  • 41
  • 58
  • Thanks for this version, seems much cleaner and obviously no XAML errors is a good thing – wonea May 01 '20 at 12:19