Thursday, October 27, 2022

XamDataGrid GotFocus event

I have a requirement to put a XamDataGrid into edit mode when it receives focus. The idea is that when a user tabs into a XamDataGrid I find the first editable cell and make it the active cell. Then I put the grid into edit mode. Sounds simple - of course it isn't.

My first attempt added a GotFocus handler but I found it was also being called as the user navigated around within the grid. Each cell raises a GotFocus event as it receives focus and, by default, this is bubbled up to the grid. There is no way to know why the event is being called (even OriginalSource isn't reliable).

Infragistics has a class called Infragistics.Windows.Helpers.FocusWithinManager. Their documentation is obtuse and the example is overly complex. It turns out to be quite simple to use (but not as simple as an event would be).

Start a new Visual Studio project (I'm using 2022). Make it a WPF App (.Net Framework) C# project and call it EditOnFocus.

We will start by subclassing the XamDataGrid. Add references to InfragisticsWPF4, InfragisticsWPF4.Editors, and InfragisticsWPF4.DataPresenter.


Add a new class called EditOnFocusXamDataGrid that inherits XamDataGrid. It needs a static constructor that registers a FocusWithinManager.

static EditOnFocusXamDataGrid()
{
    FocusWithinManager.RegisterType(typeof(EditOnFocusXamDataGrid), new System.Windows.PropertyChangedCallback(OnIsFocusWithinChanged));
}

The FocusWithinManager will call OnIsFocusWithinChanged whenever the focus changes. Let's write that method.

private static void OnIsFocusWithinChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    XamDataGrid g = (XamDataGrid)d;
    CellValuePresenter cvp;
 
    // We're a XamDataGrid and we just got focus
    if (g != null && !Convert.ToBoolean(e.OldValue) && Convert.ToBoolean(e.NewValue))
    {
        // Find the first editable cell
        foreach (DataRecord r in g.Records.Where(r => r is DataRecord).Cast<DataRecord>())
        {
            foreach (Cell c in r.Cells)
            {
                cvp = CellValuePresenter.FromCell(c);
                if (cvp != null && cvp.IsEditingAllowed)
                {
                    // Start editing the cell
                    g.ActiveCell = c;
                    g.ExecuteCommand(DataPresenterCommands.StartEditMode);
                    return;
                }
            }
        }
    }
}

This demonstration will display a text box, our new EditOnFocusXamDataGrid and another text box stacked on top of each other. When you tab from the top text box, the XamDatGrid will go into edit mode. The same will happen when you back-tab from the bottom text box.

Here's the XAML.

<Window x:Class="EditOnFocus.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:local="clr-namespace:EditOnFocus"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel Orientation="Vertical">
        <TextBox Width="100"/>
        <local:EditOnFocusXamDataGrid DataSource="{Binding Items}" Height="100">
            <local:EditOnFocusXamDataGrid.FieldLayoutSettings>
                <igDP:FieldLayoutSettings AutoGenerateFields="False"/>
            </local:EditOnFocusXamDataGrid.FieldLayoutSettings>
            <local:EditOnFocusXamDataGrid.FieldLayouts>
                <igDP:FieldLayout>
                    <igDP:FieldLayout.Fields>
                        <igDP:TextField Label="Code" Name="Code" AllowEdit="True" Width="60"/>
                        <igDP:TextField Label="Description" Name="Description" AllowEdit="False" Width="*"/>
                    </igDP:FieldLayout.Fields>
                </igDP:FieldLayout>
            </local:EditOnFocusXamDataGrid.FieldLayouts>
        </local:EditOnFocusXamDataGrid>
        <TextBox Width="100"/>
    </StackPanel>
</Window>

Here's the code behind.

using System;
using System.Collections.Generic;
using System.Windows;
 
namespace EditOnFocus
{
    public class cItem
    {
        public String Code { get; set; }
        public String Description { get; set; }
    }
 
    public partial class MainWindow : Window
    {
        public List<cItem> Items { get; set; } = new List<cItem> {
            new cItem {Code="Code A", Description="Description A"},
            new cItem {Code="Code B", Description="Description B"}
        };
 
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

When you run the demonstration, click in the top text box and hit tab. You can see the grid goes into edit mode as soon as it receives focus.



Shift-tabbing from the bottom text box works too.



Friday, October 14, 2022

Can't set both ContentTemplateSelector and ContentTemplate properties

Here's the requirement. In a combo box, display different values in the combo box and the drop down. Something like below, where I'm showing the stock number in the combo box but the stock number and description in the dropdown. In this case, the combo box is in an Infragistics XamDataGrid, but it could be stand-alone or in a Microsoft DataGrid.


The obvious way to do this is with an ItemTemplate

<Setter Property="ItemTemplate">
    <Setter.Value>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <TextBlock Width="60" Text="{Binding StockNumber}"/>
                <TextBlock Width="200" Text="{Binding Description}" TextTrimming="CharacterEllipsis"/>
            </StackPanel>
        </DataTemplate>
    </Setter.Value>
</Setter>

You specify the value to display in the combo box with DisplayMemberPath="StockNumber". But if you do this you get binding errors such as ...

System.Windows.Data Error: 25 : Both 'ContentTemplate' and 'ContentTemplateSelector' are set;  'ContentTemplateSelector' will be ignored. ComboBoxItem:'ComboBoxItem' (Name='')

WPF has got confused and thinks you're setting a ContentTemplate (which you are) and a ContentTemplateSelector (which you are not). The binding error doesn't stop the application from working correctly, but it would be nice not to see it, especially as it might mask real binding errors.

A little research on Google tells me to remove the reference to DisplayMemberPath which seems to work until you move focus off the combo box, say by clicking on the area to the right of it.


In the absence of a DisplayMemberPath WPF populated the combo box by calling the ToString method of the currently selected item. By default, this is the full class name. We got rid of the binding error, but the result is worse.

But ToString is overridable and we can override it like this.

    Public Overrides Function ToString() As String
        Return StockNumber
    End Function

To see the full demonstration of the problem and the solution, create a new Visual Studio project (WPF, VB, Framework) and call it TemplateSelectorBug. You will need to add references to the Infragistics DataPresenter and Editor assemblies. Replace MainWindow with this...

<Window x:Class="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:local="clr-namespace:TemplateSelectorBug"
        xmlns:igDP="http://infragistics.com/DataPresenter"
        xmlns:editor="http://infragistics.com/Editors"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="Template Selector Bug" Height="450" Width="800">
    <Window.Resources>
        <FrameworkElement x:Key="ProxyElement" DataContext="{Binding}"/>
    </Window.Resources>
    <Grid>
        <ContentControl Visibility="Collapsed" Content="{StaticResource ProxyElement}"/>
        <igDP:XamDataGrid DataSource="{Binding ItemDetails}" ActiveDataItem="{Binding SelectedItemDetail}">
            <igDP:XamDataGrid.FieldLayouts>
                <igDP:FieldLayout>
                    <igDP:FieldLayout.Fields>
                        <igDP:ComboBoxField Label="Stock #" Name="StockNumber" Width="70" AllowEdit="True"                                       
                              ItemsSource="{Binding DataContext.StockItems, Source={StaticResource ProxyElement}}">
                            <igDP:ComboBoxField.EditorStyle>
                                <Style TargetType="editor:XamComboEditor">
                                    <Setter Property="ComboBoxStyle">
                                        <Setter.Value>
                                            <Style TargetType="ComboBox">
                                                <Setter Property="ItemTemplate">
                                                    <Setter.Value>
                                                        <DataTemplate>
                                                            <StackPanel Orientation="Horizontal">
                                                                <TextBlock Width="60" Text="{Binding StockNumber}"/>
                                                                <TextBlock Width="200" Text="{Binding Description}" TextTrimming="CharacterEllipsis"/>
                                                            </StackPanel>
                                                        </DataTemplate>
                                                    </Setter.Value>
                                                </Setter>
                                            </Style>
                                        </Setter.Value>
                                    </Setter>
                                </Style>
                            </igDP:ComboBoxField.EditorStyle>
                        </igDP:ComboBoxField>
                    </igDP:FieldLayout.Fields>
                </igDP:FieldLayout>
            </igDP:XamDataGrid.FieldLayouts>
        </igDP:XamDataGrid>
    </Grid>
</Window>
 
and this...

Public Class cItemDetail
    Public Property StockNumber As String
End Class
 
Public Class cStock
    Public Property StockNumber As String
    Public Property Description As String
 
    'Public Overrides Function ToString() As String
    '    Return StockNumber
    'End Function
 
End Class
Class MainWindow
 
    Public Property ItemDetails = New List(Of cItemDetail)() From {New cItemDetail()}
    Public Property SelectedItemDetail As cItemDetail
 
    Public Property StockItems As New List(Of cStock)() From
    {
        New cStock() With {.StockNumber = "A", .Description = "Stock A"},
        New cStock() With {.StockNumber = "B", .Description = "Stock B"}
    }
 
End Class
 
Now run the application, select one of the stock numbers, and click to the right of the combo box. You will see this...



Now uncomment the ToString() method and repeat. You will see this...



It may not be the only way to solve the problem, but it works and isn't too intrusive.

If you want to, you can change the ComboBoxField declaration to see the binding error we are trying to get rid of.

<igDP:ComboBoxField Label="Stock #" Name="StockNumber" Width="70" AllowEdit="True" DisplayMemberPath="StockNumber"