我经常使用网络协议,这意味着可视化我通过网络收到的数据。格式始终由结构体定义,并且通过网络接收到的字节数组被转换为所述结构体。在过去的两天里,我尝试实现一个自动生成视图的控件,该控件能够递归地显示所有结构及其属性。
结构示例:
[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;
}
}
}
我希望有更熟练的人可以告诉我我缺少什么......
因此,您生成了一个能够递归显示所有结构及其属性的视图。您已经实现了用于编辑属性值的
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 或准备数据的代码隐藏中创建包装器的实例。