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 : 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

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
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
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