2

I have an object based on byte data with over 200 properties that I care about, in the sense that I want to (1) know the value, and (2) know when the value changed from one message to the next.

A snippet of the XAML I am using:

<Label Content="Prop Name" />
<TextBlock Text="{Binding PropName}" 
    Background="{Binding PropName, 
        Converter={StaticResource CompareToLastValueConverter}}" />

Currently, I have these lines pasted for EACH property, with appropriate grid location settings.

My question is this: is there a nice way to create a nested WPF UserControl that takes a generic object property from the model and handles assigning the name (with spaces) to the Label, then assigning the value of the property to the TextBlock like the example above?

Also, is this the best way to think about this problem, or am I missing a link in the "WPF way" of doing things?

tjcertified
  • 694
  • 7
  • 18

1 Answers1

2

I've often wanted to try this. I'd create an ItemsControl template for PropertyInfo.

I created a test class:

    public class MyClass
    {
        public string PropertyTest1 {get;set;}
        public string PropertyTest2 { get; set; }
        public string PropertyTest3 { get; set; }
        public string PropertyTest4 { get; set; }
    }

To display the properties of. In my data context for the display, I've got two things to bind to. A list of PropertyInfos, and the object in question. Since the PropertyInfo is static, you might be able to do this a better way using a converter or something, and not need to bind it to a property:

    public PropertyInfo[] Properties
    {
        get { return typeof(MyClass).GetProperties(); }
    }

    public MyClass MyObject
    {
        get { return new MyClass { PropertyTest1 = "test", PropertyTest3 = "Some string", PropertyTest4 = "Last Property" }; }
    }

Now, displaying the properties is easy:

<ItemsControl x:Name="PropertyDisplay" ItemsSource="{Binding Properties}" Grid.IsSharedSizeScope="True">
    <ItemsControl.Resources>
        <local:PropertyInfoValueConverter x:Key="PropertyInfoValueConverter"/>
    </ItemsControl.Resources>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <TextBlock Text="{Binding Name}" Margin="4,2"/>
                <TextBlock Grid.Column="1" Margin="4,2"/>
            </Grid>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

But these are 'static', we can't bind to any values. A way to get around that is to use the Tag property, and a multi-binding converter:

So lets add Tag="{Binding MyObject}" to our ItemsSource, and throw that and the PropertyInfo into a value converter for our second textblock:

                <TextBlock Grid.Column="1" Margin="4,2">
                    <TextBlock.Text>
                        <MultiBinding  Converter="{StaticResource PropertyInfoValueConverter}">
                            <Binding Path=""/>
                            <Binding ElementName="PropertyDisplay" Path="Tag"/>
                        </MultiBinding>
                    </TextBlock.Text>
                </TextBlock>

The converter is actually pretty simple, especially since you're not using text-boxes (so only going the read-only direction):

    public class PropertyInfoValueConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        PropertyInfo propertyInfo = values[0] as PropertyInfo;
        return propertyInfo.GetValue(values[1]);
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

This is the result:

enter image description here

You say you want spaces for the names, that could be done with a converter with some logic looking for whatever naming convention you've got (spaces before capital letters?).

It would be fun to play with template selectors to choose boolean, string, float templates and treat them differently. (Checkboxes, text, 00.00 formatted text etc)

Edit: Exploring Template Selector

Here's a sample template selector:

public class PropertyInfoTemplateSelector : DataTemplateSelector
{
    public DataTemplate StringTemplate { get; set; }
    public DataTemplate IntegerTemplate { get; set; }
    public DataTemplate DecimalTemplate { get; set; }
    public DataTemplate BooleanTemplate { get; set; }
    public DataTemplate DefaultTemplate { get; set; }

    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        PropertyInfo propertyInfo = item as PropertyInfo;
        if (propertyInfo.PropertyType == typeof(string))
        {
            return StringTemplate;
        }
        else if (propertyInfo.PropertyType == typeof(int))
        {
            return IntegerTemplate;
        }
        else if (propertyInfo.PropertyType == typeof(float) || propertyInfo.PropertyType == typeof(double))
        {
            return DecimalTemplate;
        }
        else if (propertyInfo.PropertyType == typeof(bool))
        {
            return BooleanTemplate;
        }
        return DefaultTemplate;
    }
}

Our ItemsControl is now simply:

<ItemsControl x:Name="PropertyDisplay" ItemsSource="{Binding Properties}"
              Grid.IsSharedSizeScope="True"
              Tag="{Binding MyObject}"
              ItemTemplateSelector="{StaticResource PropertyInfoTemplateSelector}"
              Margin="20"/>

I also added spaces in names using this converter:

public class PropertyInfoNameConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        string text = value as string;
        if (string.IsNullOrWhiteSpace(text))
            return string.Empty;
        StringBuilder newText = new StringBuilder(text.Length * 2);
        newText.Append(text[0]);
        for (int i = 1; i < text.Length; i++)
        {
            if (char.IsUpper(text[i]))
                if ((text[i - 1] != ' ' && !char.IsUpper(text[i - 1])) ||
                    (char.IsUpper(text[i - 1]) &&
                     i < text.Length - 1 && !char.IsUpper(text[i + 1])))
                    newText.Append(' ');
            newText.Append(text[i]);
        }
        return newText.ToString();
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

(Credit to this: https://stackoverflow.com/a/272929/1305699).

Updating our class to contain some boolean and fload fields:

enter image description here

Community
  • 1
  • 1
Joe
  • 6,773
  • 2
  • 47
  • 81