用于显示和编辑通用结构的 WPF 自定义用户控件

问题描述 投票:0回答:1

我经常使用网络协议,这意味着可视化我通过网络收到的数据。格式始终由结构体定义,并且通过网络接收到的字节数组被转换为所述结构体。在过去的两天里,我尝试实现一个自动生成视图的控件,该控件能够递归地显示所有结构及其属性。

结构示例:

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

我的实现能够按预期显示结构

我的问题在于编辑值。如果我更新为嵌套结构属性之一创建的文本框,我无法找到要更新的正确对象以使其递归地用于嵌套结构。在此特定示例中,如果我使用 Hight 进行更新,则值更改将不会应用于结构,而仅显示在文本框中。 我真的很挣扎于反射和这个问题的抽象本质。

请在下面找到我的实现:

主窗口:

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

用户控制代码:

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)
                    {
                        // Handle conversion errors if needed
                    }
                };

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

我希望有更熟练的人可以告诉我我缺少什么......

c# xaml struct reflection
1个回答
0
投票

因此,您生成了一个能够递归显示所有结构及其属性的视图。您已经实现了用于编辑属性值的

TextBox
控件。
但是,当更新嵌套结构体属性时,
TextBoxes
中的更改不会传播回结构体。

textBox.TextChanged += (sender, args) =>
{
    try
    {
        object value = Convert.ChangeType(textBox.Text, property.PropertyType);
        property.SetValue(instance, value);
        ...
    }
    catch (Exception)
    {
        // Handle conversion errors if needed
    }
};

这表明您需要一种 UI 与数据模型交互的方法,以便 UI (

TextBoxes
) 中的更改更新数据模型(结构)中的相应属性。 在 WPF 中,这通常是使用双向绑定来实现的

要实现与嵌套结构的双向绑定,您需要确保 UI 中的更改传播回数据结构。
嵌套结构可能会变得复杂,因为结构是值类型,当您访问结构的属性时,您正在使用副本,而不是原始实例。

要实现此目的,您通常需要在更改嵌套属性后替换父结构中的整个结构。
然而,WPF 的内置绑定不能很好地处理结构体等值类型。

因此,如果可能的话,尝试使用 classes 而不是结构,因为类是 引用类型 并且更容易绑定。
如果必须使用结构,请考虑在保存结构的包装类中实现

INotifyPropertyChanged
,并在属性更改时通知 UI。这样,您可以绑定到包装类,而不是直接绑定到结构。
确保每次结构体中的属性发生更改时,整个结构体都会在父对象中替换,从而触发保存该结构体的父属性的
PropertyChanged
事件。

TextBox
控件应绑定到通知 UI 更改的属性。
尝试创建一个
StructWrapper<T>
类,它包含一个结构并实现
INotifyPropertyChanged
:

public class StructWrapper<T> : INotifyPropertyChanged where T : struct
{
    private T _structInstance;

    public T StructInstance
    {
        get => _structInstance;
        set
        {
            if (!EqualityComparer<T>.Default.Equals(_structInstance, value))
            {
                _structInstance = value;
                OnPropertyChanged();
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

您需要修改您的

StructEditor
控件才能使用此
StructWrapper
并确保它在属性更改时更新整个结构。您可能需要更改 XAML 以绑定到
StructWrapper
:

<local:StructEditor StructInstance="{Binding StructWrapperInstance.StructInstance, Mode=TwoWay}"/>

在您的

StructEditor
代码隐藏中,当嵌套属性更改时,您应该替换包装类中的整个结构:

// That is a simplified example of how you might handle updates.
// You would need to expand this to handle nested properties properly.
textBox.TextChanged += (sender, args) =>
{
    try
    {
        object value = Convert.ChangeType(textBox.Text, property.PropertyType);
        property.SetValue(instance, value);
        // Notify that the entire struct has changed.
        StructInstance = instance;
        OnPropertyChanged(nameof(StructInstance));
    }
    catch (Exception)
    {
        // Handle conversion errors if needed
    }
};

最后,您需要调整绑定和属性更新逻辑以与

StructWrapper
配合使用。这可能涉及为您要编辑的每个结构创建
StructWrapper
的实例,并确保您的
StructEditor
控件可以处理这些包装器。

如果您将

StructWrapper<T>
类用作绑定源,请在 XAML 中注册它。您可能需要在 ViewModel 或准备数据的代码隐藏中创建包装器的实例。

© www.soinside.com 2019 - 2024. All rights reserved.