Search code examples
c#wpfxamlstructreflection

WPF Custom Usercontrol to show and edit generic struct


I am working a lot with network protocols and very frequently that means visualizing data i received over the network. The format is always defined by a struct, and the byte array received over the network is converted into saied struct. For the past two days i tried to implement a control that automatically generates a view, that is capable of showing all structs and their properties recursively.

Example struct:

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct sImageDimension
{
    public ushort Width { get; set; }
    public ushort Height { get; set; }
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct sVideoFormat
{
    public byte videoFormatEnabled { get; set; }
    public byte transmissionMethod { get; set; }
    public ushort transmissionCycle { get; set; }
    public sImageDimension WidthAndHeight { get; set; }
    public uint frameRate { get; set; }
    public byte Interlaced { get; set; }
    public byte colourSpace { get; set; }
    public uint maxBitrate { get; set; }
    public byte videoCompression { get; set; }
}

My Implementation is able to show the struct as intended

enter image description here

My problem lies in editing the values. If i Update a textbox that was created for one of the nested struct properties I cannot manage to find the right object to update to make it work recursivly for nested structs. In this specific example if I update with and Hight, the value changes will not be applied to the struct and only show in the textboxes. I am really struggeling with Reflection and the abstract nature of this problem.

Please find my implementation below:

MainWindow:

<local:StructEditor StructInstance="{Binding RequestPayload, Mode=TwoWay, Converter={StaticResource StructToByteArray}}"/>
<!-- For reproduction just bind it to an instance of the struct-->
<local:StructEditor StructInstance="{Binding ViewModelStructInstance}"/>
 <!-- second editor to see if the values were updated-->
<local:StructEditor StructInstance="{Binding ViewModelStructInstance}"/>

Usercontrol:

<UserControl x:Class="SomeIPTester.StructEditor"
             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:local="clr-namespace:SomeIPTester"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <ScrollViewer>
        <Border BorderBrush="HotPink" BorderThickness="5">

            <Grid>
                <StackPanel Grid.Row="1" x:Name="stackPanel" Orientation="Vertical"/>
            </Grid>
        </Border>
    </ScrollViewer>

</UserControl>

Usercontrol Code:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace SomeIPTester
{
    public partial class StructEditor : UserControl
    {
        public StructEditor()
        {
            InitializeComponent();
        }

        public static readonly DependencyProperty StructInstanceProperty =
            DependencyProperty.Register("StructInstance", typeof(object), typeof(StructEditor), new PropertyMetadata(null, OnStructInstanceChanged));

        public object StructInstance
        {
            get { return GetValue(StructInstanceProperty); }
            set 
            { 
                SetValue(StructInstanceProperty, value);
                MethodInfo method = typeof(NetworkByteOrderConverter).GetMethod("StructureToByteArray").MakeGenericMethod(TargetType);
            }
        }



        public Type TargetType
        {
            get { return (Type)this.GetValue(TargetTypeProperty); }
            set { this.SetValue(TargetTypeProperty, value); }
        }

        // Using a DependencyProperty as the backing store for TargetType.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty TargetTypeProperty =
            DependencyProperty.Register(nameof(TargetType), typeof(Type), typeof(StructEditor), new PropertyMetadata(default(Type)));


        static byte[] HexStringToByteArray(string hexString)
        {
            // Remove any spaces and convert the hex string to a byte array
            hexString = hexString.Replace(" ", "");
            int length = hexString.Length / 2;
            byte[] byteArray = new byte[length];

            for (int i = 0; i < length; i++)
            {
                byteArray[i] = System.Convert.ToByte(hexString.Substring(i * 2, 2), 16);
            }

            return byteArray;
        }

        private static void OnStructInstanceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is StructEditor structEditor && e.NewValue != null)
            {
                structEditor.GenerateControls();
            }
        }

        private void GenerateControls()
        {
            stackPanel.Children.Clear();

            if (StructInstance != null)
            {
                DisplayPropertiesRecursively(StructInstance, depth: 0);
            }
        }

        private void DisplayPropertiesRecursively(object instance, int depth)
        {
            Type type = instance.GetType();
            foreach (PropertyInfo property in type.GetProperties())
            {
                StackPanel fieldPanel = new StackPanel { Orientation = Orientation.Horizontal };

                // Label to display property name with indentation based on depth
                Label label = new Label { Content = $"{new string(' ', depth * 2)}{property.Name}", Width = 100 };
                fieldPanel.Children.Add(label);

                // TextBox for editing property value
                TextBox textBox = new TextBox
                {
                    Width = 100,
                    Text = property.GetValue(instance)?.ToString() ?? string.Empty
                };

                // Handle changes in TextBox
                textBox.TextChanged += (sender, args) =>
                {
                    // Update property when TextBox changes
                    try
                    {
                        object value = Convert.ChangeType(textBox.Text, property.PropertyType);
                        property.SetValue(instance, value);

                        // Manually trigger the update of the binding source
                        UpdateBindingSourceRecursively(instance, property.Name);
                        this.StructInstance = StructInstance;
                    }
                    catch (Exception)
                    {
                        
                    }
                };

                fieldPanel.Children.Add(textBox);
                stackPanel.Children.Add(fieldPanel);

                // Recursively display properties for nested structs or objects
                if (!property.PropertyType.IsPrimitive && property.PropertyType != typeof(string))
                {
                    object nestedInstance = property.GetValue(instance);
                    if (nestedInstance != null && !IsEnumerableType(property.PropertyType))
                    {
                        DisplayPropertiesRecursively(nestedInstance, depth + 1);
                    }
                }
            }
        }

        private bool IsEnumerableType(Type type)
        {
            return typeof(IEnumerable).IsAssignableFrom(type);
        }

        private void UpdateBindingSourceRecursively(object instance, string propertyName)
        {
            Type type = instance.GetType();
            PropertyInfo property = type.GetProperty(propertyName);

            // Manually trigger the update of the binding source for the current property
            var textBox = FindTextBoxByPropertyName(stackPanel, propertyName);
            var bindingExpression = textBox?.GetBindingExpression(TextBox.TextProperty);
            bindingExpression?.UpdateSource();

            // Recursively update the binding source for properties of properties
            if (!property.PropertyType.IsPrimitive && property.PropertyType != typeof(string))
            {
                object nestedInstance = property.GetValue(instance);
                if (nestedInstance != null)
                {
                    DisplayPropertiesRecursively(nestedInstance, depth: 1);
                }
            }
        }

        private TextBox FindTextBoxByPropertyName(StackPanel panel, string propertyName)
        {
            foreach (var child in panel.Children)
            {
                if (child is StackPanel fieldPanel)
                {
                    foreach (var fieldChild in fieldPanel.Children)
                    {
                        if (fieldChild is TextBox textBox)
                        {
                            var label = fieldPanel.Children[0] as Label;
                            if (label?.Content.ToString().Trim() == propertyName)
                            {
                                return textBox;
                            }
                        }
                    }
                }
            }
            return null;
        }
    }
}

I hope somebody more skilled can show me what i am missing...


Solution

  • It looks like you are overcomplicating things. This is a trivial problem and you really made it look complicated. You should not bother with handling any UI elements inside your control. We are presenting data models. A list of properties or key-value pairs. So, we should look to use an ItemsControl and leverage templating to create a fully dynamic view without caring about visual details when implementing the logic of the editor. We just need a TextBlock and a TextBox to present an item. Can't get much more trivial.

    The caveat is that structs are value types and structs are always copied by value.
    This means, that the value received via data binding is not the value of the data source but a copy: binding target and binding source operate on different instances.
    Hence, we have to force a property update to allow the binding source to fetch the new copy of the edited object.
    We also have to consider this fact when modifying properties that are themselves a value type.
    This boils down to the fact that every change must end with a replacement of the original source value.

    Then to dynamically generate the view, we use a TreeView that shows data items for each property and field. We host this TreeView inside a custom control.

    Performance is critical as we make heavy use of reflection (any suggestions to improve the performance are welcome).
    For example, we can store values retrieved for the type in the internal data models to avoid invoking reflection multiple times only to get the same value. We can also avoid multiple enumerations by implementing generators (use yield return).

    Another minor problem is the fact that value types are implicitly boxed but have to be unboxed explicitly: the TwoWay binding won't work on structs as the binding engine does not perform this explicit type cast. The following example unboxes the struct (when updating the binding source) by injecting an IValueConverter (if no converter was defined by the client).
    This way we can perform a generic unboxing type cast without having to bother the client with that task. This adds convenience to how the editor must be used.

    To implement the example I have used and modified existing code. So you get some extras for free.

    The key components:

    • The MemberItem is the actual representation of an edited member of the edited value type object (or reference type object). Even a reference type can define value type properties. Therefore, we have to treat reference types and value types equally in terms of presentation and updated member value propagation to the original edited object. The properties and fields of the edited object are extracted and wrapped into the MemberItem for presentation.
    • GenericUnboxingValueConverter is the generic IValueConverter used to unbox the changed object
    • TypeEditorControl is the actual control to edit both value type and reference type objects. By default, it allows to edit all public properties and fields if they are not read-only. Setting TypeEditorControl.IsEditReadOnlyMembersAllowedProperty to true allows to edit public read-only fields and properties too. The TypeEditorControl uses reflection to analyze the edited object and creates a tree data structure to present the editable object.

    enter image description here

    Implementation

    UnboxConverter.cs
    Helper class to provide a generic way of casting.

    public class UnboxConverter
    {
      public static TStruct Unbox<TStruct>(object boxedValue)
        => (TStruct)boxedValue;
    }
    

    GenericUnboxingValueConverter.cs
    The IValueConverter implementation that actually performs the unboxing of the target value of type object based on a Type parameter.

    public class GenericUnboxingValueConverter : IValueConverter
    {
      private static Dictionary<Type, MethodInfo> UnboxingConverterTableInternal { get; }
        = new Dictionary<Type, MethodInfo>();
    
      // Make converters globally available, but restrict modification access.
      public static readonly ReadOnlyDictionary<Type, MethodInfo> UnboxingConverterTable { get; }
        = new ReadOnlyDictionary<Type, MethodInfo>(GenericUnboxingValueConverter.UnboxingConverterTableInternal);
    
      // Implicit boxing --> pass through
      public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 
        => value; 
    
      // Explicit unboxing using the generic UnboxConverter
      public object? ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
      {
        bool valueIsReferencetype = !value.GetType().IsValueType;
        if (valueIsReferencetype)
        {
          // No explicit cast required.  
          // We can safely avoid the reflection to improve performance.
          return value;
        }
    
        // To improve performance, we store the created MethodInfo 
        // in a global table.
        // This reduces the costs of reflection as it is now only required once per type.
        if (!GenericUnboxingValueConverter.UnboxingConverterTable.TryGetValue(targetType, out MethodInfo genericUnboxMethodInfo))
        {    
          Type unboxConverterTypeInfo = typeof(UnboxConverter);
          MethodInfo? unboxMethodInfo = unboxConverterTypeInfo.GetMethod(nameof(UnboxConverter.Unbox));    
          genericUnboxMethodInfo = unboxMethodInfo.MakeGenericMethod(targetType);
          GenericUnboxingValueConverter.UnboxingConverterTableInternal.Add(targetType, genericUnboxMethodInfo);
        }
    
        return genericUnboxMethodInfo.Invoke(null, new[] { value });
      }
    }
    

    MemberItem.cs
    The abstract base class to present editable properties and fields. This item is the actual item that is displayed in the TreeView.
    The class stores all meta info obtained via reflection to improve performance.

    public abstract class MemberItem : INotifyPropertyChanged
    {
      public MemberItem(MemberItem? parentMemberItem, string memberName, object? memberValue, Type memberType, Type declaringType, object declaringInstance)
      {
        this.ParentMemberItem = parentMemberItem;
        this.Name = memberName;
        this.Value = memberValue;
        this.Type = memberType;
        this.IsValueIConvertibleImplementation = this.Type.GetInterface(nameof(IConvertible)) is not null;
        this.ShortTypeName = this.Type.Name!;
        this.FullyQualifiedTypeName = this.Type.FullName!;
        this.IsDeclaringTypeValueType = declaringType.IsValueType;
        this.DeclaringInstance = declaringInstance;
        this.NestedMembers = new ObservableCollection<MemberItem>();
      }
    
      protected abstract void UpdateValueFromTypeInfo();
    
    
      // For example, when the edited value of this underlying member was changed outside this editor, 
      // we have to update the item models in the TreeView. 
      // Then this method is called.
      // Because the values can include value types, 
      // we have to recursively update the reference tree of the current member.
      public void UpdateValue(bool isUpdateNestedMembersEnabled)
      {
        UpdateValueFromTypeInfo();
        if (isUpdateNestedMembersEnabled)
        {
          foreach (MemberItem nestedMember in this.NestedMembers)
          {
            nestedMember.DeclaringInstance = this.Value;
            nestedMember.UpdateValue(isUpdateNestedMembersEnabled: true);
          }
        }
      }
    
      public void AddNestedMembers(IEnumerable<MemberItem> nestedMemberItems)
      {
        foreach (MemberItem nestedMemeberItem in nestedMemberItems)
        {
          this.NestedMembers.Add(nestedMemeberItem);
        }
      }
    
      // Converts the string received from XAML (user input fields) back to the original type
      // TODO::Implement input validation to avoid an invalid cast exception
      // (for example, when an integer input field receives alphabetic input).
      public bool TryGetConvertedValue(out object? convertedValue)
      {
        convertedValue = null;
    
        if (this.Value is null)
        {
          return true;
        }
    
        if (this.IsValueIConvertibleImplementation)
        {
          // Use Convert.ChangeType to avoid additional reflection.
          //
          // TODO::Implement input validation to avoid an invalid cast exception
          // or overflow exception for integer types etc.
          convertedValue = Convert.ChangeType(this.Value, this.Type);
          return true;
        }
    
        // Try to use a TypeConverter (involves additional reflection).
        //
        // TODO::Implement input validation to avoid unexpected behavior
        // from the user perspective, because changes are displayed but were not comitted
        // due to the skipped conversion for the case the TypeConverter.IsValid() retuens FALSE).
        TypeConverter typeConverter = TypeDescriptor.GetConverter(this.Type);
        if (!typeConverter.IsValid(this.Value)
          || !typeConverter.CanConvertFrom(this.Value.GetType()))
        {
          return false;
        }
    
        convertedValue = typeConverter.ConvertFrom(this.Value);
        return true;
      }
    
      private void OnValueChanged()
        => this.ValueChanged?.Invoke(this, EventArgs.Empty);
    
      private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    
      public event PropertyChangedEventHandler? PropertyChanged;
      public event EventHandler? ValueChanged;
    
      public MemberItem? ParentMemberItem { get; }
      public string Name { get; }
      public string ShortTypeName { get; }
    
      // Excluding the assembly name
      public string FullyQualifiedTypeName { get; }
    
      public Type Type { get; }
      public object DeclaringInstance { get; private set; }
      public bool IsDeclaringTypeValueType { get; }
      public bool IsValueIConvertibleImplementation { get; }
      public ObservableCollection<MemberItem> NestedMembers { get; }
    
      private object? value;
      public object? Value
      {
        get => this.value;
        set
        {
          this.value = value;
          OnPropertyChanged();
          OnValueChanged();
        }
      }
    }
    

    PropertyItem.cs
    Item to display an editable property.

    public class PropertyItem : MemberItem
    {
      public PropertyItem(MemberItem? parentMemberItem, object declaringInstance, PropertyInfo propertyInfo)
        : base(parentMemberItem, propertyInfo.Name, propertyInfo.GetValue(declaringInstance), propertyInfo.PropertyType, propertyInfo.DeclaringType, declaringInstance)
      {
        this.PropertyInfo = propertyInfo;
      }
    
      // For example, when the property value of the underlying type 
      // was changed outside this editor, 
      // we have to update the item models in the TreeView. 
      // Then this method is called by the MemberItem base.
      protected override void UpdateValueFromTypeInfo()
      {
        this. Value = this.PropertyInfo.GetValue(this.DeclaringInstance);
      }
    
      public PropertyInfo PropertyInfo { get; set; }
    }
    

    FieldItem.cs
    Item to display an editable field.

    public class FieldItem : MemberItem
    {
      public FieldItem(MemberItem? parentMemberItem, object declaringInstance, FieldInfo fieldInfo)
        : base(parentMemberItem, fieldInfo.Name, fieldInfo.GetValue(declaringInstance), fieldInfo.FieldType, fieldInfo.DeclaringType, declaringInstance)
      {
        this.FieldInfo = fieldInfo;
      }
    
      // For example, when the field value of the underlying type 
      // was changed outside this editor, 
      // we have to update the item models in the TreeView. 
      // Then this method is called by the MemberItem base.
      protected override void UpdateValueFromTypeInfo()
        => this.Value = this.FieldInfo.GetValue(this.DeclaringInstance);
    
      public FieldInfo FieldInfo { get; set; }
    }
    

    TypeEditorControl.cs

    
    public class TypeEditorControl : Control
    {
      #region ValueProperty
      public object Value
      {
        get => (object)GetValue(ValueProperty);
        set => SetValue(ValueProperty, value);
      }
    
      public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
        "Value",
        typeof(object),
        typeof(TypeEditorControl),
        new FrameworkPropertyMetadata(
          default(object),
          FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
          OnValueChanged,
          null,
          false,
          UpdateSourceTrigger.Explicit));
    
      private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        => ((TypeEditorControl)d).OnValueChanged(e.OldValue, e.NewValue);
    
      #endregion ValueProperty
    
      #region MemberItemsProperty
    
      public ObservableCollection<MemberItem> MemberItems
      {
        get => (ObservableCollection<MemberItem>)GetValue(MemberItemsProperty);
        set => SetValue(MemberItemsPropertyKey, value);
      }
    
      private static readonly DependencyPropertyKey MemberItemsPropertyKey = DependencyProperty.RegisterReadOnly(
        "MemberItems",
        typeof(ObservableCollection<MemberItem>),
        typeof(TypeEditorControl),
        new PropertyMetadata(default));
    
      public static readonly DependencyProperty MemberItemsProperty = MemberItemsPropertyKey.DependencyProperty;
    
      #endregion MemberItemsProperty
    
      #region EditedTypeNameProperty
    
      public string EditedTypeName
      {
        get => (string)GetValue(EditedTypeNameProperty);
        set => SetValue(EditedTypeNamePropertyKey, value);
      }
    
      private static readonly DependencyPropertyKey EditedTypeNamePropertyKey = DependencyProperty.RegisterReadOnly(
        "EditedTypeName",
        typeof(string),
        typeof(TypeEditorControl),
        new PropertyMetadata(default));
    
      public static readonly DependencyProperty EditedTypeNameProperty = EditedTypeNamePropertyKey.DependencyProperty;
    
      #endregion EditedTypeNameProperty
    
      #region IsEditReadOnlyMembersAllowedProperty
    
      public bool IsEditReadOnlyMembersAllowed
      {
        get => (bool)GetValue(IsEditReadOnlyMembersAllowedProperty);
        set => SetValue(IsEditReadOnlyMembersAllowedProperty, value);
      }
    
      public static readonly DependencyProperty IsEditReadOnlyMembersAllowedProperty = DependencyProperty.Register(
    "IsEditReadOnlyMembersAllowed",
    typeof(bool),
    typeof(TypeEditorControl),
    new PropertyMetadata(default(bool), OnIsEditReadOnlyMembersAllowedChanged));
    
      private static void OnIsEditReadOnlyMembersAllowedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        => ((TypeEditorControl)d).OnIsEditReadOnlyMembersAllowedChanged((bool)e.OldValue, (bool)e.NewValue);
    
      #endregion IsEditReadOnlyMembersAllowedProperty
    
      private Type? TypeOfEditingValue { get; set; }
      private Dictionary<string, MemberItem> PropertyNameToMemberItemMap { get; }
      private bool IsChangeSourceInternal { get; set; }
      private BindingExpression ValuePropertyBindingExpression { get; set; }
    
      static TypeEditorControl()
      {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(TypeEditorControl), new FrameworkPropertyMetadata(typeof(TypeEditorControl)));
      }
    
      public TypeEditorControl()
      {
        this.MemberItems = new ObservableCollection<MemberItem>();
        this.PropertyNameToMemberItemMap = new Dictionary<string, MemberItem>();
        this.IsChangeSourceInternal = false;
        this.Loaded += OnLoaded;
      }
    
      protected override void OnInitialized(EventArgs e)
      {
        base.OnInitialized(e);
    
        BindingBase originalValuePropertyBindingBase = BindingOperations.GetBindingBase(this, ValueProperty);
    
        // If the binding object is not a Binding (e.g., a MultiBinding) or already has an IValueConverter assigned we do nothing
        if (originalValuePropertyBindingBase is not Binding originalValuePropertyBinding
          || originalValuePropertyBinding.Converter is not null)
        {
          // Custom IValueConverter defined by client. Don't override
          return;
        }
    
        // While boxing is implicit, unboxing is always an explicit cast.
        // In order to return an object to a value type property we must unbox the value.
        // We can achieve this using an IValueConverter.
        //
        // For convenience, those details are hidden from the client.
        // Instead we take care of the unboxing by injecting a generic type converter into the binding.
        // To acomplish this we have to clone the original binding as it was already sealed by the binding engine at this point.
        Binding clonedValuePropertBinding = CloneObject(originalValuePropertyBinding);
        clonedValuePropertBinding.Converter = new GenericUnboxingValueConverter();
        _ = SetBinding(ValueProperty, clonedValuePropertBinding);
        this.ValuePropertyBindingExpression = BindingOperations.GetBindingExpression(this, ValueProperty);
      }
    
      private static TObject CloneObject<TObject>(TObject originalValuePropertyBinding)
      {
        string serializedBindingInstance = XamlWriter.Save(originalValuePropertyBinding);
        var clonedValuePropertBinding = (TObject)XamlReader.Parse(serializedBindingInstance);
        return clonedValuePropertBinding;
      }
    
      private void OnLoaded(object sender, RoutedEventArgs e)
        => this.ValuePropertyBindingExpression.UpdateSource();
    
      protected virtual void OnValueChanged(object oldValue, object newValue)
      {
        if (this.IsChangeSourceInternal)
        {
          return;
        }
    
        // Stop tracking changes (in case the edited value got modified outside while being edited) of the old value
        if (this.Value is INotifyPropertyChanged oldINotifyPropertyChangedImplementation)
        {
          oldINotifyPropertyChangedImplementation.PropertyChanged -= OnEditedItemPropertyChanged;
        }
    
        this.Value = newValue;
        if (this.Value is null)
        {
          return;
        }
    
        this.TypeOfEditingValue = this.Value.GetType();
        this.EditedTypeName = this.TypeOfEditingValue.FullName ?? string.Empty;
    
        // Track changes in case the edited value got modified outside while being edited
        if (this.Value is INotifyPropertyChanged newINotifyPropertyChangedImplementation)
        {
          newINotifyPropertyChangedImplementation.PropertyChanged += OnEditedItemPropertyChanged;
        }
    
        GenerateEditableItems();
      }
    
      private void OnEditedItemPropertyChanged(object? sender, PropertyChangedEventArgs e)
      {
        if (this.IsChangeSourceInternal)
        {
          return;
        }
    
        if (string.IsNullOrWhiteSpace(e.PropertyName))
        {
          GenerateEditableItems();
          return;
        }
    
        if (this.PropertyNameToMemberItemMap.TryGetValue(e.PropertyName, out MemberItem propertyItem))
        {
          this.IsChangeSourceInternal = true;
          propertyItem.UpdateValue(isUpdateNestedMembersEnabled: true);
          this.IsChangeSourceInternal = false;
        }
      }
    
      protected virtual void OnIsEditReadOnlyMembersAllowedChanged(bool oldValue, bool newValue)
        => GenerateEditableItems();
    
      private void GenerateEditableItems()
      {
        this.MemberItems.Clear();
        this.PropertyNameToMemberItemMap.Clear();
    
        if (this.Value is null)
        {
          return;
        }
    
        IEnumerable<MemberItem> memberItems = EnumerateGeneratedMemberItemTree(null, this.TypeOfEditingValue, this.Value);
        foreach (MemberItem memberItem in memberItems)
        {
          this.MemberItems.Add(memberItem);
          if (memberItem is PropertyItem)
          {
            this.PropertyNameToMemberItemMap.Add(memberItem.Name, memberItem);
          }
        }
      }
    
      private IEnumerable<MemberItem> EnumerateGeneratedMemberItemTree(MemberItem? parentMemberItem, Type declaringtype, object declaringInstance)
      {
        PropertyInfo[] publicPropertyInfos = declaringtype.GetProperties();
        foreach (MemberItem memberItem in EnumerateGeneratedPropertyItems(publicPropertyInfos, declaringInstance, parentMemberItem))
        {
          yield return memberItem;
        }
    
        FieldInfo[] publicFieldInfos = declaringtype.GetFields();
        foreach (MemberItem memberItem in EnumerateGeneratedFieldItems(publicFieldInfos, declaringInstance, parentMemberItem))
        {
          yield return memberItem;
        }
      }
    
      private IEnumerable<MemberItem> EnumerateGeneratedPropertyItems(PropertyInfo[] publicPropertyInfos, object declaringInstance, MemberItem? parentMemberItem)
      {
        foreach (PropertyInfo propertyInfo in publicPropertyInfos)
        {
          bool isPropertyReadOnly = propertyInfo.GetSetMethod(this.IsEditReadOnlyMembersAllowed) is null;
          if (isPropertyReadOnly)
          {
            continue;
          }
    
          var newPropertyItem = new PropertyItem(parentMemberItem, declaringInstance, propertyInfo);
          Type propertyType = propertyInfo.PropertyType;
          IEnumerable<MemberItem> nestedPropertyItems = Enumerable.Empty<MemberItem>();
    
          if (!propertyType.IsPrimitive && propertyType != typeof(string))
          {
            object declaringInstanceOfNestedType = propertyInfo.GetValue(declaringInstance);
            nestedPropertyItems = EnumerateGeneratedMemberItemTree(newPropertyItem, propertyType, declaringInstanceOfNestedType);
          }
    
          newPropertyItem.AddNestedMembers(nestedPropertyItems);
          newPropertyItem.ValueChanged += OnMemberValueChanged;
    
          yield return newPropertyItem;
        }
      }
    
      private IEnumerable<MemberItem> EnumerateGeneratedFieldItems(FieldInfo[] publicFieldInfos, object declaringInstance, MemberItem? parentMemberItem)
      {
        foreach (FieldInfo fieldInfo in publicFieldInfos)
        {
          if (!this.IsEditReadOnlyMembersAllowed
            && fieldInfo.IsInitOnly)
          {
            continue;
          }
    
          var newFieldItem = new FieldItem(parentMemberItem, declaringInstance, fieldInfo);
          Type fieldType = fieldInfo.FieldType;
          IEnumerable<MemberItem> nestedFieldItems = Enumerable.Empty<MemberItem>();
    
          if (!fieldType.IsPrimitive && fieldType != typeof(string))
          {
            object declaringInstanceOfNestedType = fieldInfo.GetValue(declaringInstance);
            nestedFieldItems = EnumerateGeneratedMemberItemTree(newFieldItem, fieldType, declaringInstanceOfNestedType);
          }
    
          newFieldItem.AddNestedMembers(nestedFieldItems);
          newFieldItem.ValueChanged += OnMemberValueChanged;
    
          yield return newFieldItem;
        }
      }
    
      private void OnMemberValueChanged(object? sender, EventArgs e)
      {
        if (this.IsChangeSourceInternal)
        {
          return;
        }
    
        var memberItem = (MemberItem)sender;
        if (!memberItem.TryGetConvertedValue(out object? convertedValue))
        {
          return;
        }
    
        this.IsChangeSourceInternal = true;
    
        if (memberItem is PropertyItem propertyItem)
        {
          propertyItem.PropertyInfo.SetValue(propertyItem.DeclaringInstance, convertedValue);
        }
        else if (memberItem is FieldItem fieldItem)
        {
          fieldItem.FieldInfo.SetValue(fieldItem.DeclaringInstance, convertedValue);
        }
    
        if (memberItem.IsDeclaringTypeValueType)
        {
          // If the member type is a value type,
          // then the declaring owner still contains the original copy as value.
          // We have to update it with the "modified" value (the new copy).
          // As we don't know if the parent tree does not have another value type member,
          // we have to traverse the complete member tree.
          UpdateParentsRecursive(memberItem);
        }
    
        OnEditedInstanceChanged();
    
        this.IsChangeSourceInternal = false;
      }
    
      private void UpdateParentsRecursive(MemberItem memberItem)
      {
        if (memberItem.ParentMemberItem is null)
        {
          return;
        }
    
        MemberItem parentOwner = memberItem.ParentMemberItem;
        object newValue = memberItem.DeclaringInstance;
        if (parentOwner is PropertyItem parentPropertyItem)
        {
          parentPropertyItem.PropertyInfo.SetValue(parentPropertyItem.DeclaringInstance, newValue);
        }
        else if (parentOwner is FieldItem parentFieldItem)
        {
          parentFieldItem.FieldInfo.SetValue(parentFieldItem.DeclaringInstance, newValue);
        }
    
        UpdateParentsRecursive(parentOwner);
      }
    
      // Clear and then set the ValueProperty to trigger a change notification.
      // This is necessary because we were editing a copy (in case the ValueProperty holds a value type). 
      // This is not necessary for types tha implement INotifyPropertyChanged.
      // However, we can't rely that the type raises the PropertyChanged event for the changed property.
      // We would need to inspect the property set() for a related method call using an IL inspector
      // in order to be able to identify if the property raises the PropertyChanged event.
      // This brute force solution may cause some PropertyChanged noise (the original property change and the edited type change).
      private void OnEditedInstanceChanged()
        => this.ValuePropertyBindingExpression.UpdateSource();
    }
    

    Genric.xaml
    The default Style for the TypeEditorControl.
    TODO: nice it up.

    <ResourceDictionary>
    
      <Style TargetType="{x:Type local:TypeEditorControl}">
        <Setter Property="Template">
          <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:TypeEditorControl}">
              <Border Background="{TemplateBinding Background}"
                      BorderBrush="{TemplateBinding BorderBrush}"
                      BorderThickness="{TemplateBinding BorderThickness}">
    
                <StackPanel>
    
                  <!-- The title -->
                  <TextBlock Text="{TemplateBinding EditedTypeName}"
                             FontWeight="Bold"
                             FontSize="14" />
    
                  <!-- Property view -->
                  <TreeView ItemsSource="{TemplateBinding MemberItems}">
                    <TreeView.ItemTemplate>
                      <HierarchicalDataTemplate DataType="local:PropertyItem"
                                                ItemsSource="{Binding NestedMembers}">
                        <StackPanel Orientation="Horizontal">
                          <TextBlock Text="{Binding FullyQualifiedTypeName}"
                                     Margin="0,0,4,0" />
                          <TextBlock Text="{Binding Name}"
                                     TextTrimming="CharacterEllipsis"
                                     Width="200" />
                          <TextBox x:Name="ValueDisplay"
                                   Visibility="Collapsed"
                                   Text="{Binding Value}"
                                   Width="500" />
                        </StackPanel>
    
                        <HierarchicalDataTemplate.Triggers>
                          <DataTrigger Binding="{Binding NestedMembers.Count}"
                                       Value="0">
                            <Setter TargetName="ValueDisplay"
                                    Property="Visibility"
                                    Value="Visible" />
                          </DataTrigger>
                        </HierarchicalDataTemplate.Triggers>
                      </HierarchicalDataTemplate>
                    </TreeView.ItemTemplate>
                  </TreeView>
                </StackPanel>
              </Border>
            </ControlTemplate>
          </Setter.Value>
        </Setter>
      </Style>
    </ResourceDictionary>
    

    Usage Example

    Data.cs
    The example input data struct.

    public struct Data
    {
      public string? TextValue { get; set; }
      public Point Location { get; set; }
    
      public Data(string? text, Point location)
      {
        this.TextValue = text;
        this.Location = location;
      }
    }
    

    MainWindow.xaml

    <Window>
      <local:TypeEditorControl IsEditReadOnlyMembersAllowed="False"
                               Value="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataItem}" />
    </Window>
    

    MainWindow.xaml.cs

    public partial class MainWindow : Window
    {
      public Data DataItem
      {
        get => (Data)GetValue(DataItemProperty);
        set => SetValue(DataItemProperty, value);
      }
    
      public static readonly DependencyProperty DataItemProperty = DependencyProperty.Register(
        "DataItem",
        typeof(Data),
        typeof(MainWindow), new PropertyMetadata(default));
    
      public MainWindow()
      {
        InitializeComponent();
    
        this.DataItem = new Data("Initial text", new Point(100, 500));
      }
    }
    

    I highly recommend implementing input validation. Or at least check numeric input for overflows.