Tuesday, April 27, 2021

Bug in Infragistics XamDataGrid exporter with custom heading

 There's a weird bug I found in the Infragistics export class. It has to do with exporting a grid that has a custom header. It has a work-around, but it's ugly. Let's start off by seeing a limitation of the exporter, the Infragistics work-around, and a working work-around.

The Exporter throws an exception if you have a custom header.

Create a new VB WPF Framework project in Visual Studio 2019 and call it ExportCustomHeader. Add these references.


Here's the XAML and the code.

<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:igDP="http://infragistics.com/DataPresenter"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <RoutedCommand x:Key="Export"/>
    </Window.Resources>
    <Window.CommandBindings>
        <CommandBinding Command="{StaticResource Export}" Executed="CommandBinding_Executed"/>
    </Window.CommandBindings>
    <StackPanel Orientation="Vertical">
        <Button Content="Export" Command="{StaticResource Export}" Width="200"/>
        <igDP:XamDataGrid DataSource="{Binding Items}" Loaded="XamDataGrid_Loaded">
            <igDP:XamDataGrid.FieldSettings>
                <igDP:FieldSettings AllowSummaries="False" SummaryUIType="SingleSelect" LabelTextAlignment="Center"/>
            </igDP:XamDataGrid.FieldSettings>
            <igDP:XamDataGrid.FieldLayoutSettings>
                <igDP:FieldLayoutSettings AutoGenerateFields="False"/>
            </igDP:XamDataGrid.FieldLayoutSettings>
            <igDP:XamDataGrid.FieldLayouts>
                <igDP:FieldLayout>
                    <igDP:FieldLayout.Fields>
                        <igDP:CheckBoxField Name="IsSelected">
                            <igDP:CheckBoxField.Label>
                                <StackPanel Orientation="Horizontal">
                                    <CheckBox/>
                                    <TextBlock Text="Selected"/>
                                </StackPanel>
                            </igDP:CheckBoxField.Label>
                        </igDP:CheckBoxField>
                        <igDP:TextField Label="Description" Name="Description"/>
                    </igDP:FieldLayout.Fields>
                </igDP:FieldLayout>
            </igDP:XamDataGrid.FieldLayouts>
        </igDP:XamDataGrid>
    </StackPanel>
</Window>

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

Imports System.ComponentModel
Imports Infragistics.Windows.DataPresenter
Imports Infragistics.Windows.DataPresenter.ExcelExporter
 
Class MainWindow
    Implements INotifyPropertyChanged
 
    Private xdg As XamDataGrid
    Public Class cItem
 
        Public Property IsSelected As Boolean
        Public Property Description As String
    End Class
 
    Private _Items As Collections.Generic.List(Of cItem)
 
    Public ReadOnly Property Label As String = "HI MOM"
    Public Sub New()
        InitializeComponent()
 
        Items = New List(Of cItem)() From {
            New cItem() With {.Description = "A description", .IsSelected = True},
            New cItem() With {.Description = "Another description", .IsSelected = False}
        }
    End Sub
 
    Public Property Items As Collections.Generic.List(Of cItem)
        Get
            Return _Items
        End Get
        Set(value As Collections.Generic.List(Of cItem))
            SetProperty(_Items, value)
        End Set
    End Property
 
    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
    Private Sub NotifyPropertyChanged(PropertyName As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(PropertyName))
    End Sub
    Public Function SetProperty(Of T)(ByRef storage As T, value As T, <System.Runtime.CompilerServices.CallerMemberName> Optional PropertyName As String = Nothing) As Boolean
        If Object.Equals(storage, value) Then Return False
        storage = value
        NotifyPropertyChanged(PropertyName)
        Return True
    End Function
 
    Private Sub CommandBinding_Executed(sender As Object, e As ExecutedRoutedEventArgs)
        Dim excelExporter As New Infragistics.Windows.DataPresenter.ExcelExporter.DataPresenterExcelExporter()
        Dim FilePath As String = System.IO.Path.Combine(System.IO.Path.GetTempPath(), System.IO.Path.GetTempFileName()).Replace("tmp", "xlsx")
        excelExporter.Export(xdg, FilePath, Infragistics.Documents.Excel.WorkbookFormat.Excel2007)
        Process.Start(FilePath)
    End Sub
 
    Private Sub XamDataGrid_Loaded(sender As Object, e As RoutedEventArgs)
        xdg = DirectCast(sender, XamDataGrid)
    End Sub
End Class

If you run this project you will see the Exporter throws an exception "System.Windows.Controls.StackPanel is not a supported cell value". Fair enough, it doesn't know how to extract a text from a stack panel. But you can provide a HeaderLabelExporting event handler which provides a value. Let's do that.

Modify CommandBinding_Executed to specify an event handler.

    Private Sub CommandBinding_Executed(sender As Object, e As ExecutedRoutedEventArgs)
        Dim excelExporter As New Infragistics.Windows.DataPresenter.ExcelExporter.DataPresenterExcelExporter()
        Dim FilePath As String = System.IO.Path.Combine(System.IO.Path.GetTempPath(), System.IO.Path.GetTempFileName()).Replace("tmp", "xlsx")
        AddHandler excelExporter.HeaderLabelExporting, AddressOf excelExporter_HeaderLabelExporting
        excelExporter.Export(xdg, FilePath, Infragistics.Documents.Excel.WorkbookFormat.Excel2007)
        RemoveHandler excelExporter.HeaderLabelExporting, AddressOf excelExporter_HeaderLabelExporting
        Process.Start(FilePath)
    End Sub

and the event handler...

    Private Sub excelExporter_HeaderLabelExporting(sender As Object, e As HeaderLabelExportingEventArgs)
        If e.Field.Name = "IsSelected" Then
            e.Label = "SELECTED"
        End If
    End Sub

Now we can do the export, but look what happens to our custom header. Here's the screen before and after the export. Do you see the problem?



I contacted Infragistics support (they're better than average) and got a response in 24 hours. Without admitting this is a bug, they provided a work-around that involves styling the label presenter to override the template. I tweaked their solution a bit because eventually I needed to do this in code. Change the XAML to look like 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:igDP="http://infragistics.com/DataPresenter"
        xmlns:local="clr-namespace:BindXamToDataSet"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow2" Height="450" Width="800">
    <Window.Resources>
        <RoutedCommand x:Key="Export"/>
    </Window.Resources>
    <Window.CommandBindings>
        <CommandBinding Command="{StaticResource Export}" Executed="CommandBinding_Executed"/>
    </Window.CommandBindings>
    <StackPanel Orientation="Vertical">
        <Button Content="Export" Command="{StaticResource Export}" Width="200"/>
        <igDP:XamDataGrid DataSource="{Binding Items}" Loaded="XamDataGrid_Loaded">
            <igDP:XamDataGrid.FieldSettings>
                <igDP:FieldSettings AllowSummaries="False" SummaryUIType="SingleSelect" LabelTextAlignment="Center" AllowEdit="False"/>
            </igDP:XamDataGrid.FieldSettings>
            <igDP:XamDataGrid.FieldLayoutSettings>
                <igDP:FieldLayoutSettings AutoGenerateFields="False"/>
            </igDP:XamDataGrid.FieldLayoutSettings>
            <igDP:XamDataGrid.FieldLayouts>
                <igDP:FieldLayout>
                    <igDP:FieldLayout.Fields>
                        <igDP:CheckBoxField Name="IsSelected">
                            <igDP:CheckBoxField.Settings>
                                <igDP:FieldSettings>
                                    <igDP:FieldSettings.LabelPresenterStyle>
                                        <Style TargetType="igDP:LabelPresenter">
                                            <Setter Property="ContentTemplate">
                                                <Setter.Value>
                                                    <DataTemplate>
                                                        <StackPanel Orientation="Horizontal">
                                                            <CheckBox/>
                                                            <TextBlock Text="Selected"/>
                                                        </StackPanel>
                                                    </DataTemplate>
                                                </Setter.Value>
                                            </Setter>
                                        </Style>
                                    </igDP:FieldSettings.LabelPresenterStyle>
                                </igDP:FieldSettings>
                            </igDP:CheckBoxField.Settings>
                        </igDP:CheckBoxField>
                        <igDP:TextField Label="Description" Name="Description"/>
                    </igDP:FieldLayout.Fields>
                </igDP:FieldLayout>
            </igDP:XamDataGrid.FieldLayouts>
        </igDP:XamDataGrid>
    </StackPanel>
</Window>

You can see their solution is far more complicated and is functionally equivalent to the original XAML but for some reason the Exporter is OK with it