2

I need to be able to resolve a (string) path to a property of a class, like WPF binding does. So something like "MyProperty.MySubProperty.MyList[2].FinalProperty". I do not need to subscribe to changes, I just need to resolve it once to get the value.

Is there any code I can reuse of the WPF framework, or anything else that already exists?

Coder14
  • 1,305
  • 1
  • 9
  • 26
  • Just found a few questions/answers that come close, but if possible I would like to have the full WPF binding syntax. And if possible, without reinventing the wheel... https://stackoverflow.com/questions/18870162/get-object-value-by-string-path https://stackoverflow.com/questions/1954746/using-reflection-in-c-sharp-to-get-properties-of-a-nested-object – Coder14 May 21 '21 at 07:24
  • Would be interested in a solution, too. As even if you have a `BindingExpression` I found no way to get the source property without using the property name string. – Klamsi May 21 '21 at 07:30
  • 1
    The bindings path is hold and manipulated using a dedicate type: [PropertyPath](https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/PropertyPath.cs). Most of its methods are `internal` and it's totally not easy to understand the implementation and how to use it. It is much easier to write own implementation to resolve concrete property from path using recursion. – Sinatr May 21 '21 at 08:29

2 Answers2

2

I need to be able to resolve a (string) path to a property of a class, like WPF binding does. So something like "MyProperty.MySubProperty.MyList[2].FinalProperty". I do not need to subscribe to changes, I just need to resolve it once to get the value.

It is totally possible to get the value of any path like that. Without knowing the values I put something together that might be able to help.

I'm not an expert at reflection, and it has no error handling at all so you'll need to modify and extend it if satisfies your need.

One thing to note, I couldn't figure out how to resolve the path without a object reference ( the StartClass start = new StartClass(); at the beginning). I'll leave that as an exercise for the reader.

class Program
{
    public static void Main()
    {
        StartClass start = new StartClass();

        string path = "MyProperty.MySubProperty.MyList[2].FinalProperty";

        string[] props = path.Split('.');

        var propertyValue = GetPropertyValue(start, props[0]);

        for (int i = 1; i < props.Length; i++)
        {
            ref string prop = ref props[i];

            // check to see if the property is an indexed property
            if (prop.Contains("[") && prop.Contains("]"))
            {
                // split MyList[2] into MyList 2]
                string[] split = prop.Split("[");

                // get the value of MyList
                propertyValue = GetPropertyValue(propertyValue, split[0]);

                // get the number in 2]
                string rawIndex = split[1].Replace("]", "");

                // parse the number to an actual number
                if (int.TryParse(rawIndex, out int index))
                {
                    // make sure the property is an enumerable, wouldn't make much sense to use an index on it if it wasn't
                    if (propertyValue is IEnumerable enumerable)
                    {
                        propertyValue = enumerable.Cast<object>().ElementAt(index);
                    }
                    else
                    {
                        throw new NotSupportedException("Attempted to index non-enumerable object");
                    }
                }
                else
                {
                    throw new NotSupportedException("Index isn't integer, ranges supported at this time.");
                }
            }
            else
            {
                // if the property wasn't an indexed property, just get the next value of the next property
                propertyValue = GetPropertyValue(propertyValue, prop);
            }
        }

        Console.WriteLine(propertyValue);
        // outputs: 12
    }

    public static object GetPropertyValue(object property, string PropertyName)
    {
        var propertyInfo = property.GetType().GetProperty(PropertyName);
        return propertyInfo.GetValue(property);
    }
}

public class StartClass
{
    public A MyProperty { get; set; } = new();

}

public class A
{
    public B MySubProperty { get; set; } = new B();
}

public class B
{
    public List<C> MyList { get; set; } = new List<C>() {
        new C(),
        new C(),
        new C(),
    };
}

public class C
{
    public int FinalProperty { get; set; } = 12;
}
DekuDesu
  • 2,224
  • 1
  • 5
  • 19
  • 1
    Not a problem I hope it's the right solution for you're problem! Make sure to add lots of error handing, this code makes **ALOT** of assumptions that values are in the right place. – DekuDesu May 21 '21 at 11:13
2

If you need to parse completely 100% equivalent to the binding, then it is better to use a simple proxy with a binding.

Here is my simple implementation of such a proxy. In it, in addition to getting the property value, you can also listen to its change, find out if the binding is set and in what state it is.

using System;
using System.Windows;
using System.Windows.Data;


namespace Proxy
{
    /// <summary> Provides a <see cref="DependencyObject"/> proxy with
    /// one property and an event notifying about its change. </summary>
    public class ProxyDO : DependencyObject
    {
        /// <summary> Property for setting external bindings. </summary>
        public object Value
        {
            get { return (object)GetValue(ValueProperty); }
            set { SetValue(ValueProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Value.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register(nameof(Value), typeof(object), typeof(ProxyDO), new PropertyMetadata(null));

        protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            base.OnPropertyChanged(e);
            ValueChanged?.Invoke(this, e);
        }

        /// <summary> An event that occurs when the value of any
        /// <see cref="DependencyProperty"/> of this object changes.</summary>
        public event EventHandler<DependencyPropertyChangedEventArgs> ValueChanged;

        /// <summary> Returns <see langword="true"/> if the property value <see cref="Value"/> is not set.</summary>
        public bool IsUnsetValue => Equals(ReadLocalValue(ValueProperty), DependencyProperty.UnsetValue);

        /// <summary> Clears all <see cref="DependencyProperty"/> this <see cref="ProxyDO"/>.</summary>
        public void Reset()
        {
            LocalValueEnumerator locallySetProperties = GetLocalValueEnumerator();
            while (locallySetProperties.MoveNext())
            {
                DependencyProperty propertyToClear = locallySetProperties.Current.Property;
                if (!propertyToClear.ReadOnly)
                {
                    ClearValue(propertyToClear);
                }
            }

        }

        /// <summary> <see langword="true"/> if the property <see cref="Value"/> has Binding.</summary>
        public bool IsValueBinding => BindingOperations.GetBindingExpressionBase(this, ValueProperty) != null;

        /// <summary> <see langword="true"/> if the property <see cref="Value"/> has a binding
        /// and it is in the state <see cref="BindingStatus.Active"/>.</summary>
        public bool IsActiveValueBinding
        {
            get
            {
                var exp = BindingOperations.GetBindingExpressionBase(this, ValueProperty);
                if (exp == null)
                    return false;
                var status = exp.Status;
                return status == BindingStatus.Active;
            }
        }

        /// <summary>Setting the Binding to the Property <see cref="Value"/>.</summary>
        /// <param name="binding">The binding to be assigned to the property.</param>
        public void SetValueBinding(BindingBase binding)
            => BindingOperations.SetBinding(this, ValueProperty, binding);
    }
}

An example of using a proxy:

    ProxyDO proxy;
    public void MainMetod()
    {
        // Create Proxy.
        proxy = new ProxyDO();

        //An example of adding a wiretapping method, if necessary.
        proxy.ValueChanged += OnValueChanged; 

        // Setting Binding.
        string propertyPath = "All string";
        proxy.SetValueBinding(new Binding(propertyPath));

        object currentValue = proxy.Value;
    }

    private void OnValueChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        //Listening code
    }

P.S. Shown in a very simplified form. When creating a binding, for correct operation, you must specify other parameters in addition to the path. At least Source is an object in which, according to the specified property path, this source property will be searched for.

EldHasp
  • 6,079
  • 2
  • 9
  • 24