Control de usuario personalizado de WPF para mostrar y editar estructuras genéricas
Trabajo mucho con protocolos de red y muy frecuentemente eso significa visualizar datos que recibí a través de la red. El formato siempre está definido por una estructura y la matriz de bytes recibida a través de la red se convierte en dicha estructura. Durante los últimos dos días intenté implementar un control que genera automáticamente una vista, que es capaz de mostrar todas las estructuras y sus propiedades de forma recursiva.
Estructura de ejemplo:
[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; }
}
Mi implementación puede mostrar la estructura según lo previsto
Mi problema radica en editar los valores. Si actualizo un cuadro de texto que se creó para una de las propiedades de estructura anidada, no puedo encontrar el objeto correcto para actualizar y que funcione de forma recursiva para estructuras anidadas. En este ejemplo específico, si actualizo con Hight, los cambios de valor no se aplicarán a la estructura y solo se mostrarán en los cuadros de texto. Realmente estoy luchando con la Reflexión y la naturaleza abstracta de este problema.
Encuentre mi implementación a continuación:
Ventana principal:
<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}"/>
Control de usuario:
<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>
Código de control de usuario:
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;
}
}
}
Espero que alguien más experto pueda mostrarme lo que me falta...
Entonces generas una vista capaz de mostrar todas las estructuras y sus propiedades de forma recursiva. Y ha implementado TextBox
controles para editar valores de propiedades.
Pero al actualizar las propiedades de la estructura anidada, los cambios en TextBoxes
no se propagan a la estructura.
textBox.TextChanged += (sender, args) =>
{
try
{
object value = Convert.ChangeType(textBox.Text, property.PropertyType);
property.SetValue(instance, value);
...
}
catch (Exception)
{
// Handle conversion errors if needed
}
};
Eso sugiere que necesita un método para que la interfaz de usuario interactúe con el modelo de datos de modo que los cambios en la interfaz de usuario ( TextBoxes
) actualicen las propiedades correspondientes en el modelo de datos (estructuras). En WPF, esto normalmente se logra mediante un enlace bidireccional .
Para lograr un enlace bidireccional con estructuras anidadas, deberá asegurarse de que los cambios en la interfaz de usuario se propaguen nuevamente a la estructura de datos.
Eso puede volverse complejo con estructuras anidadas porque las estructuras son tipos de valor , y cuando accede a una propiedad de una estructura, está trabajando con una copia , no con la instancia original.
Para que esto funcione, normalmente necesita reemplazar toda la estructura en la estructura principal, después de cambiar una propiedad anidada.
Sin embargo, el enlace integrado de WPF no maneja esto bien para tipos de valores como estructuras.
Por lo tanto, intente utilizar clases en lugar de estructuras si es posible, ya que las clases son tipos de referencia y son más sencillas de vincular.
Si debe usar estructuras, considere implementar INotifyPropertyChanged
una clase contenedora que contenga la estructura y notifique a la interfaz de usuario cuando cambia una propiedad. De esa manera, puede vincularse a la clase contenedora en lugar de hacerlo directamente a la estructura.
Asegúrese de que cada vez que cambie una propiedad dentro de la estructura, toda la estructura se reemplace en el objeto principal, lo que desencadena el PropertyChanged
evento para la propiedad principal que contiene la estructura.
Los TextBox
controles deben estar vinculados a propiedades que notifiquen los cambios a la interfaz de usuario.
Intente crear una StructWrapper<T>
clase que contenga una estructura e implemente 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));
}
}
Deberá modificar su StructEditor
control para que funcione con esto StructWrapper
y asegurarse de que actualice toda la estructura cuando cambie una propiedad. Es posible que tengas que cambiar tu XAML para vincularlo a StructWrapper
:
<local:StructEditor StructInstance="{Binding StructWrapperInstance.StructInstance, Mode=TwoWay}"/>
En su StructEditor
código subyacente , debe reemplazar toda la estructura en la clase contenedora cuando cambia una propiedad anidada:
// 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
}
};
Finalmente, deberá ajustar la lógica de actualización de propiedades y enlaces para que funcione con el archivo StructWrapper
. Eso puede implicar crear instancias StructWrapper
para cada estructura que desee editar y asegurarse de que su StructEditor
control pueda manejar estos contenedores.
Registre la StructWrapper<T>
clase en XAML si la está utilizando como fuente vinculante. Probablemente necesitarás crear instancias del contenedor en tu ViewModel o en el código subyacente donde estás preparando los datos.
La idea completa de mis esfuerzos anteriores era hacer de este un control genérico que pudiera funcionar en la infraestructura existente y proporcionar una interfaz para ver y editar los paquetes de protocolos.
En mi caso, estamos hablando de cientos de tipos de estructuras diferentes para diferentes protocolos de red.
Conozco MVVM y el funcionamiento de los enlaces. Simplemente no veo cómo podría aplicarlo a mi problema, que es trabajar con estructuras con propiedades. Por eso intenté adoptar el enfoque más tradicional con cuadros de texto y eventos.
Por lo tanto, se necesita un control genérico que pueda manejar dinámicamente varios tipos de estructuras sin la sobrecarga de crear manualmente clases contenedoras para cada una.
Dado que las estructuras son tipos de valor, cualquier cambio realizado en la propiedad de una estructura anidada no afectará la estructura original a menos que toda la estructura anidada se restablezca a la estructura principal: necesitará actualizar la estructura principal cada vez que cambie una propiedad anidada: Puede usar una pila o una lista para realizar un seguimiento de la cadena principal de propiedades. Cuando te sumerges en estructuras anidadas, envía la información de la propiedad principal a la pila. Cuando vuelvas a subir (cuando la recursividad se relaje), extrae la información de la propiedad de la pila.
Cuando se actualiza un TextBox correspondiente a una propiedad de estructura anidada, use la pila para caminar hasta la raíz de la jerarquía de estructuras, actualizando cada estructura a lo largo del camino.
Una representación conceptual de cómo se podría manejar el TextChanged
evento para estructuras anidadas sería:
textBox.TextChanged += (sender, args) =>
{
try
{
object value = Convert.ChangeType(textBox.Text, property.PropertyType);
PropertyInfo[] parentProperties = ...; // The stack of parent properties leading to this property
object rootInstance = ...; // The root instance of the struct hierarchy
// Start from the leaf property and work back up to the root
for (int i = parentProperties.Length - 1; i >= 0; i--)
{
PropertyInfo currentProperty = parentProperties[i];
object currentInstance = currentProperty.GetValue(rootInstance);
// Update the property in the current instance
property.SetValue(currentInstance, value);
if (i > 0)
{
// Set the updated instance to its parent
PropertyInfo parentProperty = parentProperties[i - 1];
object parentInstance = parentProperty.GetValue(rootInstance);
parentProperty.SetValue(parentInstance, currentInstance);
}
else
{
// The top-level parent has been reached; update the root instance
StructInstance = rootInstance;
}
}
}
catch (Exception ex)
{
// Handle conversion errors if needed
}
};
Esta es solo una guía conceptual de alto nivel y deberá ampliarse con la lógica real para administrar la pila de propiedades y manejar la instancia raíz.
Pero la idea principal sigue siendo navegar reflexivamente por la jerarquía de estructuras, aplicando los cambios necesarios. Eso también significaría que su DisplayPropertiesRecursively
método deberá realizar un seguimiento de la cadena de propiedades.
Eso... parece complejo, debido a la naturaleza de las estructuras en C#. Y si las estructuras son grandes o están profundamente anidadas, el rendimiento puede convertirse en una preocupación debido a la reflexión y copia repetida de instancias de estructuras.