.NET, C#, MVVM, UWP, WPF, XAML

Displaying Details Property Grid in XAML

Most data driven application usually have some header and detail screens. For the header screen, we usually put them in a ListView or an ItemsControl, set the ItemsSource and ItemTemplate, then boom, you have your neat looking list view. For this post, I’ll be talking about the details screen.

The most obvious way to create a details screen is to create a grid with two column, one for the label and one for the value.

<Grid>
  <Grid.RowDefinitions>
    <RowDefinitions />
    <RowDefinitions />
    <RowDefinitions />
  </Grid.RowDefinitions>
  <Grid.ColumnDefinitions>
    <ColumnDefinitions />
    <ColumnDefinitions />
  </Grid.ColumnDefinitions>
  <TextBlock Text="Name" Grid.Row="0" Grid.Column="0"/>
  <TextBlock Text="{Binding Name}" Grid.Row="0" Grid.Column="1"/>
  <TextBlock Text="Address" Grid.Row="1" Grid.Column="0"/>
  <TextBlock Text="{Binding Address}" Grid.Row="1" Grid.Column="1"/>
  <TextBlock Text="Phone" Grid.Row="2" Grid.Column="0"/>
  <TextBlock Text="{Binding Phone}" Grid.Row="2" Grid.Column="1"/>
  <!-- and so on.. -->
</Grid>	

This approach works fine, however it’s really hard to maintain this kind of xaml. If you need to add a new row in the middle of the long list of rows, you have to adjust all the rows that will be affected. And of course, you’re going to have a very large xaml file and you’ll have a hard time reading it.

Whenever I deal with any problems in xaml, I always try to look in to the four techniques that usually solves my problem. These are converters, behavior, attached properties and custom controls. For this problem, I think we can best solve it using a behavior. I won’t elaborate on this, I’ll probably create a new post that will discuss these techniques.

Preparation
First thing we have to do is to prepare the class that we want to use as details. We want to display some or all of the properties of this class in the details page. How do we identify those properties that we want to display? We’ll be using a custom attribute PropertyGridItemAttribute.

 

  [AttributeUsage(AttributeTargets.Property)]
  public class PropertyGridItemAttribute : Attribute
  {
    public PropertyGridItemAttribute(string group = null)
    {
      this.Group = group ?? "default";
    }

    public PropertyGridItemAttribute(string header = null, int order = 0) : this(null)
    {
      this.Header = header;
      this.Order = order;
    }

    public PropertyGridItemAttribute(string group, string header, int order = 0) : this(group)
    {
      this.Header = header;
      this.Order = order;
    }

    public string Group { get; set; }
    public string Header { get; set; }
    public int? Order { get; set; }
  }

We’ll be using this attribute to decorate our properties so that we’ll know how to identify each properties that we display in the property grid. At the top of the class, there’s an AttributeUsage.Property. It only means that the attribute should be used in a property.

To identify the properties that should appear in our details class we’ll use the

PropertyGridItemAttribute on each properties.

  public class Customer : ViewModelBase
  {
    private string _Name = "Lance";
    [PropertyGridItem("Name", 1)]
    public string Name
    {
      get { return _Name; }
      set { Set(ref _Name, value); }
    }

    private string _Address = "3 Richards St., Hackensack, NJ";
    [PropertyGridItem("Delivery Address", 2)]
    public string Address
    {
      get { return _Address; }
      set { Set(ref _Address, value); }
    }

    private bool _IsCashOnDelivery = true;
    [PropertyGridItem("Cash on Delivery", 3)]
    public bool IsCashOnDelivery
    {
      get { return _IsCashOnDelivery; }
      set { Set(ref _IsCashOnDelivery, value); }
    }
  }

PropertyGridBehavior
What we wanna do is to be able to use a single template for all the details that we want to display and we want to put it in a container (clue: we’ll use ItemsContol) and we want it to be as reusable as possible.

So now, I’m going to create a behavior that we can use on every ItemsControl (that includes ListView and ListBox too). This behavior will have two dependency properties. The Source Object and the Group. I’ll start explaining the SourceObject first then the group later. The source object is the details class, in our example it’s the customer class. Whenever the source object property changes we generate a list of grid items and set it to the items source of the ItemsControl. So we won’t be binding anything to the ItemsSource of the ItemsControl in our xaml. Instead, we will bind to the SourceObject of the behavior.

  public object SourceObject
    {
      get { return (object)GetValue(SourceObjectProperty); }
      set { SetValue(SourceObjectProperty, value); }
    }

    public static readonly DependencyProperty SourceObjectProperty =
        DependencyProperty.Register("SourceObject", typeof(object), typeof(PropertyGridBehavior), new PropertyMetadata(null, SourceObjectChanged));

    private static void SourceObjectChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
      PropertyGridBehavior behavior = d as PropertyGridBehavior;
      (d as PropertyGridBehavior).AssociatedObject.ItemsSource = new ObservableCollection<PropertyGridItem>(GenerateGridItems(behavior.Group, e.NewValue));
    }

	The grid items is a list iof GridItems class that has a Header and a value. The header should be bound to the label. We'll see that later in the xaml part. 

    class PropertyGridItem
	{
		public string Header { get; set; }
		public object Value { get; set; }
	}

The grid items is a list iof GridItems class that has a Header and a value. The header should be bound to the label. We’ll see that later in the xaml part.

    class PropertyGridItem
	{
		public string Header { get; set; }
		public object Value { get; set; }
	}

To generate the GridItems, we take each properties that has a PropertyGridItemAttribute and wrap it inside a PropertyGridItem.

private static IEnumerable<PropertyGridItem> GenerateGridItems(string groupName, object sourceObject)
    {
      if (groupName == null)
        groupName = string.Empty; 

      var properties = from i in sourceObject.GetType().GetTypeInfo().DeclaredProperties.Where(p => p.CustomAttributes.Count(c => c.AttributeType == typeof(PropertyGridItemAttribute)) > 0)
                       where (groupName != string.Empty && i.GetCustomAttribute<PropertyGridItemAttribute>().Group == groupName) || groupName == string.Empty
                       orderby i.GetCustomAttribute<PropertyGridItemAttribute>().Order
                       select i;

      foreach (var property in properties)
      {
        int ctr = 0;
        string header = property.GetCustomAttribute<PropertyGridItemAttribute>().Header;
        int? order = property.GetCustomAttribute<PropertyGridItemAttribute>().Order;
        if (header == null)
          header = property.Name;
        if (order == null)
          order = ctr;
        ctr++;
        yield return new PropertyGridItem() { Header = header, Value = property.GetValue(sourceObject) };
      }
    }

Here’s the source code of the behavior class:

 public class PropertyGridBehavior : Behavior<ItemsControl>
  {
    public object SourceObject
    {
      get { return (object)GetValue(SourceObjectProperty); }
      set { SetValue(SourceObjectProperty, value); }
    }

    public static readonly DependencyProperty SourceObjectProperty =
        DependencyProperty.Register("SourceObject", typeof(object), typeof(PropertyGridBehavior), new PropertyMetadata(null, SourceObjectChanged));

    public string Group
    {
      get { return (string)GetValue(GroupProperty); }
      set { SetValue(GroupProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Group.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty GroupProperty =
        DependencyProperty.Register("Group", typeof(string), typeof(PropertyGridBehavior), new PropertyMetadata(null));

    private static void SourceObjectChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
      PropertyGridBehavior behavior = d as PropertyGridBehavior;
      (d as PropertyGridBehavior).AssociatedObject.ItemsSource = new ObservableCollection<PropertyGridItem>(GenerateGridItems(behavior.Group, e.NewValue));
    }

    private static IEnumerable<PropertyGridItem> GenerateGridItems(string groupName, object sourceObject)
    {
      if (groupName == null)
        groupName = string.Empty;

      var properties = from i in sourceObject.GetType().GetTypeInfo().DeclaredProperties.Where(p => p.CustomAttributes.Count(c => c.AttributeType == typeof(PropertyGridItemAttribute)) > 0)
                       where (groupName != string.Empty && i.GetCustomAttribute<PropertyGridItemAttribute>().Group == groupName) || groupName == string.Empty
                       orderby i.GetCustomAttribute<PropertyGridItemAttribute>().Order
                       select i;

      foreach (var property in properties)
      {
        int ctr = 0;
        string header = property.GetCustomAttribute<PropertyGridItemAttribute>().Header;
        int? order = property.GetCustomAttribute<PropertyGridItemAttribute>().Order;
        if (header == null)
          header = property.Name;
        if (order == null)
          order = ctr;
        ctr++;
        yield return new PropertyGridItem() { Header = header, Value = property.GetValue(sourceObject) };
      }
    }
  }

Now in the XAML part, we’ll add a PropertyGridBehavior in the ItemsControl. This will generate a list of PropertyGridItem from all the properties attributed with PropertyGridAttribute in the Customer class. We also define the DataTemplate in the xaml. Keep in mind that when we use this behavior we’ll be binding to a list of PropertyGridItem. PropertyGridItem has 2 properties, a Header which is the label and a Value.

	  <ItemsControl HorizontalAlignment="Stretch">
        <i:Interaction.Behaviors>
          <CustomBehavior:PropertyGridBehavior SourceObject="{Binding Customer}"/>
        </i:Interaction.Behaviors>
        <ItemsControl.ItemTemplate>
          <DataTemplate>
            <Grid HorizontalAlignment="Stretch">
              <Grid.ColumnDefinitions>
                <ColumnDefinition />
                <ColumnDefinition />
              </Grid.ColumnDefinitions>
              <TextBlock Grid.Column="0" Text="{Binding Header}" />
              <TextBlock Grid.Column="1" Text="{Binding Value}" />
              <Border BorderBrush="Silver" BorderThickness="0,0,0,1" Grid.ColumnSpan="2"/>
            </Grid>
          </DataTemplate>
        </ItemsControl.ItemTemplate>
      </ItemsControl>

Grouping
If we have a lot of details in the class, we would probably want to display it as groups. That’s when we will use the grouping dependency property of the PropertyGridBehavior. We will also have to specify the group when we put the attribute in the class. Let’s go back to the example. This time we’re going to specify the group name in the first parameter of the PropertyGridItemAttribute. We can also specify the order in the last parameter of the constructor. The order will tell the order of the property within the group when displayed in the grid.


    public class Customer : ViewModelBase
    {
      private string _Name = "Lance";
      [PropertyGridItem("PersonalInfo", "Name", 1)]
      public string Name
      {
        get { return _Name; }
        set { Set(ref _Name, value); }
      }

      private DateTime _BirthDay;
      [PropertyGridItem("PersonalInfo", "Birthday", 2)]
      public DateTime BirthDay
      {
        get { return _BirthDay; }
        set { Set(ref _BirthDay, value); }
      }

      private string _Address = "3 Richards St., Hackensack, NJ";
      [PropertyGridItem("DeliveryInfo", "Delivery Address", 1)]
      public string Address
      {
        get { return _Address; }
        set { Set(ref _Address, value); }
      }

      private bool _IsCashOnDelivery = true;
      [PropertyGridItem("DeliveryInfo", "Cash On Delivery", 2)]
      public bool IsCashOnDelivery
      {
        get { return _IsCashOnDelivery; }
        set { Set(ref _IsCashOnDelivery, value); }
      }
    }

We also need to specify the group in the PropertyGridBehavior in the xaml. This time we’ll have two ItemsControl, one for each group. We can also use a resource for the item template so we don’t have to duplicate our code.

 <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.Resources>
      <DataTemplate x:Key="gridItemTemplate">
        <Grid Margin="0,10,0,10" HorizontalAlignment="Stretch">
          <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
          </Grid.ColumnDefinitions>
          <Border Grid.ColumnSpan="2"                   BorderBrush="Gray"                   BorderThickness="0,0,0,1" />
          <TextBlock Grid.Column="0"                      Margin="2,5,2,5"                      FontWeight="Bold"                      Text="{Binding Header}" />
          <TextBlock Grid.Column="1"                      Margin="2,5,2,5"                      HorizontalAlignment="Right"                      Text="{Binding Value}" />
        </Grid>
      </DataTemplate>
    </Grid.Resources>
    <StackPanel>
      <StackPanel Width="500">
        <TextBlock Margin="20"                    FontSize="32"                    FontWeight="Bold"                    Text="Personal Info" />
        <ItemsControl Margin="20"                       HorizontalAlignment="Stretch"                       ItemTemplate="{StaticResource gridItemTemplate}">
          <i:Interaction.Behaviors>
            <behavior:PropertyGridBehavior Group="PersonalInfo" SourceObject="{Binding}" />
          </i:Interaction.Behaviors>
        </ItemsControl>
      </StackPanel>
      <StackPanel Width="500">
        <TextBlock Margin="20"                    FontSize="32"                    FontWeight="Bold"                    Text="Delivery Info" />
        <ItemsControl Margin="20"                       HorizontalAlignment="Stretch"                       ItemTemplate="{StaticResource gridItemTemplate}">
          <i:Interaction.Behaviors>
            <behavior:PropertyGridBehavior Group="DeliveryInfo" SourceObject="{Binding}" />
          </i:Interaction.Behaviors>
        </ItemsControl>
      </StackPanel>

    </StackPanel>
  </Grid>

Here’s a screen shot of the sample application
PropertyGrid.PNG

 

You can download the source code from my github page.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s