Wednesday, October 11, 2023

Common XamDataGrid TemplateField techniques

I had an colleague ask me how to create a column of buttons in a XamDataGrid. He had been trying for a while and hadn't got anywhere. I told him he had to use a TemplateField, but then I realized how different the bindings for a TemplateField are, so I thought it would be a good idea to write this blog entry.

We will be looking at how to create and bind a TemplateField, specifically one containing a button.

Start a new Visual Studio WPF project using C# and .Net Core. I used .Net Core 7. Call it ButtonField. Add references to InfragisticsWPF4, Editors, and DataPresenter. Your version numbers may be different.


We will start by writing an app to display a list of employee names and salaries. Replace MainWindow XAML and code behind with this.

<Window x:Class="ButtonField.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:igEd="http://infragistics.com/Editors"
        xmlns:local="clr-namespace:ButtonField"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="Button Field" Height="450" Width="800">
    <igDP:XamDataGrid DataSource="{Binding Employees}" GroupByAreaLocation="None">
        <igDP:XamDataGrid.FieldSettings>
            <igDP:FieldSettings AllowEdit="False"/>
        </igDP:XamDataGrid.FieldSettings>
        <igDP:XamDataGrid.FieldLayoutSettings>
            <igDP:FieldLayoutSettings AutoGenerateFields="False"/>
        </igDP:XamDataGrid.FieldLayoutSettings>
        <igDP:XamDataGrid.FieldLayouts>
            <igDP:FieldLayout>
                <igDP:FieldLayout.Fields>
                    <igDP:TextField Label="Name" Name="Name"/>
                    <igDP:CurrencyField Label="Salary" Name="Salary"/>
                </igDP:FieldLayout.Fields>
            </igDP:FieldLayout>
        </igDP:XamDataGrid.FieldLayouts>
    </igDP:XamDataGrid>
</Window>

------------------------------------------------------------------------------------------------------------

using System.Collections.Generic;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
 
namespace ButtonField
{
    [ObservableObject]
    public partial class cEmployee
    {
        [ObservableProperty] public string _name;
        [ObservableProperty] public decimal _salary;
 
        public cEmployee(string name, decimal salary)
        {
            this.Name = name;
            this.Salary = salary;  
        }
    }
 
    public partial class MainWindow : Window
    {
        public List<cEmployee> Employees { get; set; } = new List<cEmployee>()
        {
            new cEmployee("john smith",  100000),
            new cEmployee("mary jones", 120000),
            new cEmployee("omaha wilson", 90000)
        };
 
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

If you run this you will see a very simple grid.


Now we will add a button on each row that gives the employee a 10% salary raise. As we are adding a button we will need a command for that button.

    <Window.Resources>
        <RoutedCommand x:Key="GiveRaiseCommand"/>
    </Window.Resources>
    <Window.CommandBindings>
        <CommandBinding Command="{StaticResource GiveRaiseCommand}" Executed="GiveRaise_Executed"/>
    </Window.CommandBindings>

Add a the GiveRaise_Executed method to the code behind. 

        using System.Windows.Input;

        private void GiveRaise_Executed(object sender, ExecutedRoutedEventArgs e)
        {
        }

Add the basic TemplateField to the XAML after the name field.

<igDP:TemplateField Label="Give 10% Raise" AlternateBinding="{Binding Path=Salary}" Width="auto">
    <igDP:TemplateField.DisplayTemplate>
        <DataTemplate>
            <Button Content="Give Raise Command="{StaticResource GiveRaiseCommand}"/>
        </DataTemplate>
    </igDP:TemplateField.DisplayTemplate>
</igDP:TemplateField>

Now we have a button but it doesn't do much.


Let's have the content of the button tell us how much each user's raise will be. It will be different for each user and normally we would use a CellBinding to achieve this but you can't use CellBindings on TemplateFields.

We have bound the TemplateField to the employee's salary. We can add a converter to calculate 10% of the salary and bind the button's content to that. We can also specify a ContentStringFormat on the button to add the words "Give Raise". Change the TemplateField to look like this.

<igDP:TemplateField Label="Give 10% Raise" AlternateBinding="{Binding Path=Salary, Converter={StaticResource TenPercentConverter}}" Width="auto">
    <igDP:TemplateField.DisplayTemplate>
        <DataTemplate>
            <Button Content="{igEd:TemplateEditorValueBinding}" ContentStringFormat="Give Raise {0:C}" Command="{StaticResource GiveRaiseCommand}"/>
        </DataTemplate>
    </igDP:TemplateField.DisplayTemplate>
</igDP:TemplateField>

Note the igEd:TemplateEditorValueBinding markup extension takes the TemplateField's binding after the converter is applied but ignores any StringFormat. The StringFormat has to applied on the button itself so we use a ContentStringFormat.

To create the converter, add a class called Converters. It will look like this.

using System;
using System.Globalization;
using System.Windows.Data;
 
namespace ButtonField
{
    internal class TenPercentConverter : IValueConverter
    {
        public object Convert(Object value, Type targetType, object parameter, CultureInfo culture)
        {
            try
            {
                return decimal.Parse(value.ToString()) / 10;
            }
          catch { return 0; }
        }
 
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

Add a reference to the converter in the Windows.Resources section.

    <Window.Resources>
        <local:TenPercentConverter x:Key="TenPercentConverter"/>
        <RoutedCommand x:Key="GiveRaiseCommand"/>
    </Window.Resources>

 If you run the application right now you will see the content of the button is populated, but it still doesn't do anything.


Because we used a command instead of writing a Click event handler, we can guarantee the grid's active data item will be correct in the _Executed event handler. There are several possible ways of  identifying which button was clicked. This is ours. Make the event handler look like this.

using Infragistics.Windows.DataPresenter;

private void GiveRaise_Executed(object sender, ExecutedRoutedEventArgs e)
{
    XamDataGrid xdg = (XamDataGrid)e.Source;
    cEmployee employee = (cEmployee) xdg.ActiveDataItem;
    employee.Salary *= 1.1M;
}

Run the application now and click the first button. The Salary is updated in the salary column and also in the button.




No comments:

Post a Comment