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
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>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 : DependencyObject02. {03. public static readonly DependencyProperty PropertyNameProperty = DependencyProperty04. .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 Binding10. {11. Path = new PropertyPath($"ParentRow.DataContext[{e.NewValue}]"),12. RelativeSource = new RelativeSource13. {14. AncestorType = typeof(GridViewCell)15. },16. Mode = BindingMode.TwoWay17. };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
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
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 ItemsSourceProperty16. {17. get { return (string)GetValue(ItemsSourcePropertyProperty); }18. set { SetValue(ItemsSourcePropertyProperty, value); }19. }20. 21. public string SelectedValueProperty22. {23. get { return (string)GetValue(SelectedValuePropertyProperty); }24. set { SetValue(SelectedValuePropertyProperty, value); }25. }26. 27. public BindingMode BindingMode28. {29. get { return (BindingMode)GetValue(BindingModeProperty); }30. set { SetValue(BindingModeProperty, value); }31. }32. 33. public IValueConverter Converter34. {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 bindings50. 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 Binding60. {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 : IValueConverter82. {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 = ColumnsProvider02. .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 GridViewColumnGroup26. {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
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