Wednesday, June 9, 2021

Binding in an Infragistics TemplateField

Binding in an Infragistics TemplateField is pretty funky. It's tempting to bind the TemplateField to the entire DataItem, then pull off the properties as needed. For example, if TemplateText and fontSize are properties of the collection we are bound to, then...

       <igDP:TemplateField Label="Template Item" AlternateBinding="{Binding}">
            <igDP:TemplateField.DisplayTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding TemplateText}" FontSize="{Binding fontSize}"/>
                </DataTemplate>
            </igDP:TemplateField.DisplayTemplate>
        </igDP:TemplateField>

seems to work until we try to implement DataValueChangedNotificationsActive="True" which does not trigger for template fields defined as above. Because we are binding the TemplateField to an object we cannot detect when changes are made to properties of that object. If we want to trigger DataValueChanged events for any field then the field has to be bound to a property, not an object. ie.

        <igDP:TemplateField Label="Template Item" Name="TemplateText">

and the templated controls have to be bound using Infragistic's markup extension  {igEditors:TemplateEditorValueBinding} like this...

        <igDP:TemplateField Label="Template Item" Name="TemplateText">
            <igDP:TemplateField.DisplayTemplate>
                <DataTemplate>
                    <TextBlock Text="{igEditors:TemplateEditorValueBinding}"/>
                </DataTemplate>
            </igDP:TemplateField.DisplayTemplate>
            <igDP:TemplateField.EditTemplate>
                <DataTemplate>
                    <TextBox Text="{igEditors:TemplateEditorValueBinding}"/>
                </DataTemplate>
            </igDP:TemplateField.EditTemplate>
        </igDP:TemplateField>

Our TemplateField will now raise DataValueChanged events when the TemplateText is modified. But how do we bind other properties such as FontSize. According to Infragistics help (which is world-class) we need a XAML snippet like this...

FontSize="{Binding Path=(igEditors:TemplateEditor.Editor).DataContext.DataItem.fontSize, RelativeSource={RelativeSource Self}}"

Which seems like a lot of XAML for one little binding. Perhaps we could write a markup extension to make it a bit more concise.

Start a new Visual Studio 2019 WPF Application (Core) project in C# and call it XamDataGridTemplatedField. Add a class called TemplateBinding. We will inherit from MarkupExtension so we will need a constructor and a ProvideValue method. The markup will have the format {TemplateBinding pathname}. We will assume two-way and property-changed for brevity.

When a markup extension is inside a template, the ProvideValue is called with a target object of type "SharedDp" which is an internal class that acts as a placeholder because the target object doesn't actually exist yet. If we return ourselves we will be called again once the target object does exist. This is just weird templatey stuff. Eventually we call ProvideValueInternal. If we are not inside a template we will call ProvideValueInternal on the first call.

using System;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
 
namespace XamDataGridTemplatedField
{
    // Support template bindings for XamDataGrids
    class TemplateBinding : MarkupExtension
    {
        private String Path;
        private DependencyProperty dp;
        private FrameworkElement fe;
 
        public TemplateBinding(String path)
        {
            this.Path = path;
        }
 
        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            IProvideValueTarget pvt = (serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget);
            dp = pvt.TargetProperty as DependencyProperty;
            if (pvt.TargetObject.GetType().Name == "SharedDp")
                return this;
            else
            {
                fe = pvt.TargetObject as FrameworkElement;
                return ProvideValueInternal(dp, fe);
            }
        }
    }
}

If we already have a DataContext we are ready to create the binding otherwise we will have to wait until we have been initialized.

        private object ProvideValueInternal(DependencyProperty dp, FrameworkElement fe)
        {
            if (fe.DataContext == null)
            {
                AddInitializedHandler(fe);
                return dp.DefaultMetaData.DefaultValue;
            }
            else
                return CreateBinding(Path, dp, fe);
        }

        private void AddInitializedHandler(FrameworkElement fe)
        {
            fe.Initialized += FieldBinding_Initialized;
        }
 
        private void FieldBinding_Initialized(object sender, EventArgs e)
        {
            CreateBinding(Path, dp, fe);
            fe.Initialized -= FieldBinding_Initialized;
        }

Eventually we will call CreateBinding. You can see we are simply reproducing, in code, the XAML that Infragistics gave us.

        private object CreateBinding(String Path, DependencyProperty dp, FrameworkElement fe)
        {
            Binding b = new Binding("(igEditors:TemplateEditor.Editor).DataContext.DataItem." + Path);
            b.RelativeSource = new RelativeSource(RelativeSourceMode.Self);
            b.Mode = BindingMode.TwoWay;
            b.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
            fe.SetBinding(dp, b);
            return fe.GetValue(dp);
        }

Now we have our markup extension, here's some XAML that uses it. Change MainWindow.xaml to look like this.

<Window x:Class="XamDataGridTemplatedField.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:igDP="http://infragistics.com/DataPresenter"
        xmlns:igEditors="http://infragistics.com/Editors"
        xmlns:local="clr-namespace:XamDataGridTemplatedField"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <igDP:XamDataGrid DataSource="{Binding Items}" DataValueChanged="XamDataGrid_DataValueChanged" >
        <igDP:XamDataGrid.FieldLayoutSettings>
            <igDP:FieldLayoutSettings AutoGenerateFields="False"/>
        </igDP:XamDataGrid.FieldLayoutSettings>
        <igDP:XamDataGrid.FieldSettings>
            <igDP:FieldSettings DataValueChangedNotificationsActive="True" DataItemUpdateTrigger="OnCellValueChange"/>
        </igDP:XamDataGrid.FieldSettings>
        <igDP:XamDataGrid.FieldLayouts>
            <igDP:FieldLayout>
                <igDP:TemplateField Label="Template Item" Name="TemplateItem">
                    <igDP:TemplateField.DisplayTemplate>
                        <DataTemplate>
                            <TextBlock Text="{igEditors:TemplateEditorValueBinding}" FontSize="{local:TemplateBinding fontSize}"/>
                        </DataTemplate>
                    </igDP:TemplateField.DisplayTemplate>
                    <igDP:TemplateField.EditTemplate>
                        <DataTemplate>
                            <TextBox Text="{igEditors:TemplateEditorValueBinding}" FontSize="{local:TemplateBinding fontSize}" />
                        </DataTemplate>
                    </igDP:TemplateField.EditTemplate>
                </igDP:TemplateField>
            </igDP:FieldLayout>
        </igDP:XamDataGrid.FieldLayouts>
    </igDP:XamDataGrid>
</Window>

and change the MainWindow.xaml.vb to this...

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Media;
 
namespace XamDataGridTemplatedField
{
    public class cItem
    {
        public string TemplateItem { get; set; }
        public double fontSize { get; set; }
    }
 
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        public void SetProperty<T>(ref T storage, T value, [CallerMemberName] string name = "")
        {
            if (!Object.Equals(storage, value))
            {
                storage = value;
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
        }
 
        private List<cItem> _Items = new List<cItem>() {
            new cItem() { TemplateItem="TABLE", fontSize=6 },
            new cItem() { TemplateItem="CHAIR", fontSize=20 }
        };
 
        public List<cItem> Items
        {
            get { return _Items; }
            set { SetProperty(ref _Items, value); }
        }
 
        public MainWindow()
        {
            InitializeComponent();
        }
 
        private void XamDataGrid_DataValueChanged(object sender, Infragistics.Windows.DataPresenter.Events.DataValueChangedEventArgs e)
        {
            MessageBox.Show(string.Format("You changed the {0} in row {1} to {2}",
                e.Field.GetType().Name,
                e.Record.Index,
                e.CellValuePresenter.Value));
        }
    }
}

If you run the application you can see the binding to Text and FontSize are both working.


If you modify one of the texts, you will see the DataValueChanged event is raised.



No comments:

Post a Comment