This is a migrated thread and some comments may be shown as answers.

Dynamic Grid Columns

6 Answers 788 Views
GridView
This is a migrated thread and some comments may be shown as answers.
Maurice
Top achievements
Rank 1
Maurice asked on 22 Jul 2018, 03:19 PM

I am trying to get the GridView to work in a rather complex scenario that involves numerous dynamic columns all with their own editing and display criteria. I have come up with a solution that involves storing a metadata object on the column and then I bind to the properties on the metadata object in the Tag property.

This approach has been working fine or non-dynamic columns but I am unable to successfully determine the property to bind to in the cell template as the property name is decided at runtime, hence why I store it in the DynamicPropertyName property in the metadata.

 

I tried using a MultiValueConverter which works fine for the ConvertTo, but I am unable to provide the correct values in the ConvertBack.

This is my current cell template with the binding issue highlighted - if there is a way to also bind the Column.Tag.DynamicPropertyName property on the ParentRow. object which is a DynamicObject, then this approach would work in its current form.

 

 

 

 

6 Answers, 1 is accepted

Sort by
0
Maurice
Top achievements
Rank 1
answered on 22 Jul 2018, 03:34 PM

I was hoping my picture would display inline - here is the XAML for the template selector that I am using (unsuccessfully) as I cannot access the dynamic property name in the dynamic row data context.

 

01.<controls:GridCellTemplateSelector x:Key="Grid_Rate_CellTemplateSelector">
02.    <controls:GridCellTemplateSelector.NewRowCellTemplate>
03.      <DataTemplate />
04.    </controls:GridCellTemplateSelector.NewRowCellTemplate>
05.     
06.    <controls:GridCellTemplateSelector.GridRowCellTemplate>
07.      <DataTemplate>
08.        <telerik:RadMaskedNumericInput HorizontalAlignment="Stretch" SelectionOnFocus="SelectAll"
09.                                       Style="{StaticResource GridCellNumericInputStyle}" TextMode="PlainText" AllowInvalidValues="False" UpdateValueEvent="PropertyChanged" IsClearButtonVisible="False"
10.                                       Value="{Binding Path=ParentRow.DataContext[Column.Tag.DynamicPropertyName], RelativeSource={RelativeSource AncestorType={x:Type telerik:GridViewCell}}}"
11.                                       Mask="{Binding Path=Column.Tag.InputMask, RelativeSource={RelativeSource AncestorType={x:Type telerik:GridViewCell}}}"
12.                                       maskedInput:MaskedInputExtensions.Minimum="{Binding Path=Column.Tag.MinValue, RelativeSource={RelativeSource AncestorType={x:Type telerik:GridViewCell}}}"
13.                                       maskedInput:MaskedInputExtensions.Maximum="{Binding Path=Column.Tag.MaxValue, RelativeSource={RelativeSource AncestorType={x:Type telerik:GridViewCell}}}" />
14.      </DataTemplate>
15.    </controls:GridCellTemplateSelector.GridRowCellTemplate>
16.  </controls:GridCellTemplateSelector>
0
Maurice
Top achievements
Rank 1
answered on 22 Jul 2018, 05:40 PM

OK, so I went with an attached property, but I am by no means delighted with this approach.

I also tried an Interaction Behavior but the associated objects bindings were not being bound, which makes me think the behavior is bound before the control on which it is declared.

XAML:

1.<telerik:RadMaskedNumericInput behaviors:DynamicBindingBehavior.PropertyName="{Binding Path=Column.Tag.DynamicPropertyName, RelativeSource={RelativeSource AncestorType={x:Type telerik:GridViewCell}}}" />

 

Behavior:

01.public class DynamicBindingBehavior : DependencyObject
02.  {
03.    public static readonly DependencyProperty PropertyNameProperty = DependencyProperty
04.      .RegisterAttached("PropertyName", typeof(string), typeof(DynamicBindingBehavior), new PropertyMetadata(OnPropertyNamePropertyChanged));
05.
06.    private static void OnPropertyNamePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
07.    {
08.      var input = d as RadMaskedNumericInput;
09.      var binding = new Binding
10.      {
11.        Path = new PropertyPath($"ParentRow.DataContext[{e.NewValue}]"),
12.        RelativeSource = new RelativeSource
13.        {
14.          AncestorType = typeof(GridViewCell)
15.        },
16.        Mode = BindingMode.TwoWay
17.      };
18.
19.      input.SetBinding(RadMaskedNumericInput.ValueProperty, binding);
20.    }
21.
22.    public static string GetPropertyName(DependencyObject obj) => (string)obj.GetValue(PropertyNameProperty);
23.
24.    public static void SetPropertyName(DependencyObject obj, string value) => obj.SetValue(PropertyNameProperty, value);
25.  }
 

Hopefully this illustrates more clearly what the issue is, and how I need to resolve it and somebody can assist with a cleaner, more generic and nicer-all-around solution.

 

Regards,
Maurice

0
Maurice
Top achievements
Rank 1
answered on 24 Jul 2018, 06:11 AM
I resolved the issue by extending the controls I was using in the grid templates and implementing the binding within those new control implementations.
0
Dilyan Traykov
Telerik team
answered on 24 Jul 2018, 11:46 AM
Hello Maurice,

I'm glad to hear that you've come up with a solution that suits your scenario.

Feel free to share your exact approach as I'm sure it will be beneficial to the community.

If I can assist you in any other way, please let me know.

Regards,
Dilyan Traykov
Progress Telerik
Want to extend the target reach of your WPF applications, leveraging iOS, Android, and UWP? Try UI for Xamarin, a suite of polished and feature-rich components for the Xamarin framework, which allow you to write beautiful native mobile apps using a single shared C# codebase.
0
Maurice
Top achievements
Rank 1
answered on 24 Jul 2018, 12:39 PM

Hi Dilyan,

After some refactoring for a more generic approach, I ended up going with a Behavior. The idea is that the behavior can be attached to any UIElement and by binding a as a string, the control will then create a binding to the AssociatedObjects DataContext using that string as the bindings property path. It still needs some tests around it and some more DependencyProperties added in for value bindings on other control, but what I will show below is working nicely and allows me to bind to the column name [and other things] that are in the Tag property of each column, thereby giving total freedom over which columns are added to the grid and how those columns will behave.

Behavior:

01.public class DynamicPropertyBindingBehavior : Behavior<UIElement>
02.  {
03.    public static readonly DependencyProperty SelectedValuePropertyProperty =
04.      DependencyProperty.Register("SelectedValueProperty", typeof(string), typeof(DynamicPropertyBindingBehavior), new PropertyMetadata(OnSelectedValuePropertyChanged));
05.
06.    public static readonly DependencyProperty ItemsSourcePropertyProperty =
07.      DependencyProperty.Register("ItemsSourceProperty", typeof(string), typeof(DynamicPropertyBindingBehavior), new PropertyMetadata(OnItemSourcePropertyChanged));
08.
09.    public static readonly DependencyProperty BindingModeProperty =
10.      DependencyProperty.Register("BindingMode", typeof(BindingMode), typeof(DynamicPropertyBindingBehavior), new PropertyMetadata(BindingMode.OneWay));
11.
12.    public static readonly DependencyProperty ConverterProperty =
13.        DependencyProperty.Register("Converter", typeof(IValueConverter), typeof(DynamicPropertyBindingBehavior), new PropertyMetadata());
14.
15.    public string ItemsSourceProperty
16.    {
17.      get { return (string)GetValue(ItemsSourcePropertyProperty); }
18.      set { SetValue(ItemsSourcePropertyProperty, value); }
19.    }
20.
21.    public string SelectedValueProperty
22.    {
23.      get { return (string)GetValue(SelectedValuePropertyProperty); }
24.      set { SetValue(SelectedValuePropertyProperty, value); }
25.    }
26.
27.    public BindingMode BindingMode
28.    {
29.      get { return (BindingMode)GetValue(BindingModeProperty); }
30.      set { SetValue(BindingModeProperty, value); }
31.    }
32.
33.    public IValueConverter Converter
34.    {
35.      get { return (IValueConverter)GetValue(ConverterProperty); }
36.      set { SetValue(ConverterProperty, value); }
37.    }
38.
39.    private static void OnItemSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
40.      (d as DynamicPropertyBindingBehavior).SetBinding(e.NewValue.ToString(), "ItemsSourceProperty");
41.
42.    private static void OnSelectedValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
43.      (d as DynamicPropertyBindingBehavior).SetBinding(e.NewValue.ToString(), "SelectedValueProperty");
44.
45.    private static void OnValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
46.      (d as DynamicPropertyBindingBehavior).SetBinding(e.NewValue.ToString(), "ValueProperty");
47.
48.    // NOTE: it is assumed that the Value Binding property for the attached UIElement is set last in the XAML, allowing for other bindings
49.    //       for BindingMode, Converter, etc. to be set first for use in the value binding itself, which is done in this function for all bindings
50.    private void SetBinding(string propertyName, string dependencyPropertyName)
51.    {
52.      var property = AssociatedObject.GetType().GetField(dependencyPropertyName, BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy);
53.      var dp = (DependencyProperty)property.GetValue(AssociatedObject);
54.      if (dp == null)
55.      {
56.        throw new ArgumentException($"Unable to find the {dependencyPropertyName} DependencyProperty on the {AssociatedObject.GetType().Name} DependecyObject.");
57.      }
58.
59.      var binding = new Binding
60.      {
61.        Path = new PropertyPath(propertyName),
62.        Mode = BindingMode,
63.        Converter = Converter ?? new DynamicValueConverter()
64.      };
65.
66.      var methodName = "SetBinding";
67.      var method = AssociatedObject.GetType()
68.        .GetMethods()
69.        .Where(x => x.Name == methodName)
70.        .FirstOrDefault(x => x.GetParameters().Last().ParameterType == typeof(BindingBase));
71.
72.      if (method == null)
73.      {
74.        throw new ArgumentException($"Unable to find the {methodName} method on the {AssociatedObject.GetType().Name} DependecyObject.");
75.      }
76.
77.      method.Invoke(AssociatedObject, new object[] { dp, binding });
78.    }
79.  }
80.
81.  internal class DynamicValueConverter : IValueConverter
82.  {
83.    private static Logger Logger = LogManager.GetCurrentClassLogger();
84.
85.    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
86.    {
87.      if (value == null) { return null; }
88.      if (!(value is IConvertible)) { return value; }
89.
90.      return System.Convert.ChangeType(value, Nullable.GetUnderlyingType(targetType) ?? targetType);
91.    }
92.
93.    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => value;
94.  }

 

Usage from XAML:

01.<DataTemplate>
02.        <telerik:RadComboBox Style="{StaticResource GridCellComboBoxStyle}">
03.          <i:Interaction.Behaviors>
04.            <behaviors:DynamicPropertyBindingBehavior BindingMode="TwoWay"
05.                ItemsSourceProperty="{Binding Path=Column.Tag.BindingValue, RelativeSource={RelativeSource AncestorType={x:Type telerik:GridViewCell}}}"
06.                SelectedValueProperty="{Binding Path=Column.Tag.BindingProperty, RelativeSource={RelativeSource AncestorType={x:Type telerik:GridViewCell}}}" />
07.          </i:Interaction.Behaviors>
08.        </telerik:RadComboBox>
09.      </DataTemplate>

 

When I am creating the dynamic datasource, I also create a metadata object that is then put into the Tag property of the grid in the OnAutoGeneratingColumn handler for the grid.

Creating the metadata:

01.descriptor = ColumnsProvider
02.                .Configure(
03.                  x => x.GroupName = groupName,
04.                  x => x.GroupHeader = groupHeader,
05.                  x => x.Header = meta.ColumnHeader,
06.                  x => x.DataType = meta.TypeCode.AsType(),
07.                  x => x.CellTemplateSelector = cellTemplateSelector,
08.                  x => x.BindingProperty = name,
09.                  x => x.BindingValue = bindingValueName,
10.                  x => x.Format = meta.Format,
11.                  x => x.AllowNull = meta.AllowNull,
12.                  x => x.Mask = meta.Mask,
13.                  x => x.MaxValue = meta.MaxValue,
14.                  x => x.MinValue = meta.MinValue);

 

A custom behavior to handle column generation and attach the metadata:

01.private void OnAutoGeneratingColumn(object sender, GridViewAutoGeneratingColumnEventArgs e)
02.    {
03.      var columnName = e.ItemPropertyInfo.Name;
04.      var descriptor = Provider.GetColumnDescriptor(columnName);
05.
06.      var column = e.Column as GridViewDataColumn;
07.      column.Tag = descriptor;
08.
09.      column.Header = descriptor.Header;
10.      column.DataType = descriptor.DataType ?? column.DataType;
11.      column.TextAlignment = descriptor.TextAlignment;
12.      column.Width = descriptor.Width.HasValue ? new GridViewLength(descriptor.Width.Value) : GridViewLength.Auto;
13.      column.IsResizable = descriptor.IsResizable;
14.      column.IsSortable = descriptor.IsSortable;
15.      column.IsReadOnly = descriptor.IsReadOnly;
16.      column.DataFormatString = descriptor.Format;
17.      column.CellTemplateSelector = descriptor.CellTemplateSelector;
18.      column.CellStyleSelector = descriptor.CellStyleSelector;
19.      column.ColumnGroupName = descriptor.GroupName;
20.
21.      if (!string.IsNullOrEmpty(descriptor.GroupName))
22.      {
23.        if (!(AssociatedObject.ColumnGroups.FirstOrDefault(x => x.Name == descriptor.GroupName) is GridViewColumnGroup group))
24.        {
25.          AssociatedObject.ColumnGroups.Add(new GridViewColumnGroup
26.          {
27.            Name = descriptor.GroupName,
28.            Header = string.IsNullOrEmpty(descriptor.GroupHeader) ? descriptor.GroupName : descriptor.GroupHeader,
29.            HeaderTemplate = Provider.GetGroupHeaderTemplate(descriptor.GroupName)
30.          });
31.        }
32.      }
33.    }

 

The end result is that the grid is totally driven from metadata that built from some hardcoded values, but also from dynamic values from an API that describes the data [columns and columngroups] that needs to be displayed in the grid.

 

Regards,
Maurice

 

 

 

 

 

 

 

 

 

 

 

0
Dilyan Traykov
Telerik team
answered on 25 Jul 2018, 12:29 PM
Hello Maurice,

Thank you very much for the detailed description and code snippets. As a thank you for your efforts, I've awarded you with some Telerik points.

Regards,
Dilyan Traykov
Progress Telerik
Want to extend the target reach of your WPF applications, leveraging iOS, Android, and UWP? Try UI for Xamarin, a suite of polished and feature-rich components for the Xamarin framework, which allow you to write beautiful native mobile apps using a single shared C# codebase.
Tags
GridView
Asked by
Maurice
Top achievements
Rank 1
Answers by
Maurice
Top achievements
Rank 1
Dilyan Traykov
Telerik team
Share this question
or