Search code examples
wpfxamldatagrid

WPF Datagrid Expander's icon changed when scrolling down and up


I'm working on a WPF app, where I have a datagrid with custom expander. The issue I'm encountering is when the user click on the + button, it changes to - and the content shows up, but when I scroll down and then scroll up, the icon changed back to + but the content stays the same. I got to know that it's because of the datagrid's virtualization. I can add EnableRowVirtualization="False" to the DataGrid property but it slows down the performance so much that the users would definitely notice it (as the list might contain thousands of rows). Is there a way to work around this?

Here is my code:

<UserControl
    x:Class="ProjectName.View.HomePage.OutstandingPatientDataGrid">

    <UserControl.Resources>
       // Other styles
        <Style TargetType="{x:Type DataGridCell}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type DataGridCell}">
                        <Grid Background="{TemplateBinding Background}">
                            <ContentPresenter VerticalAlignment="Center" />
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <Style TargetType="{x:Type DataGridRow}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type DataGridRow}">
                        <Border>
                            <SelectiveScrollingGrid>
                                <SelectiveScrollingGrid.ColumnDefinitions>
                                    <ColumnDefinition Width="Auto" />
                                    <ColumnDefinition Width="*" />
                                </SelectiveScrollingGrid.ColumnDefinitions>
                                <SelectiveScrollingGrid.RowDefinitions>
                                    <RowDefinition Height="*" />
                                    <RowDefinition Height="Auto" />
                                </SelectiveScrollingGrid.RowDefinitions>
                                <DataGridCellsPresenter
                                    Grid.Column="1"
                                    ItemsPanel="{TemplateBinding ItemsPanel}"
                                    SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                                <DataGridDetailsPresenter
                                    Grid.Row="1"
                                    Grid.Column="1"
                                    SelectiveScrollingGrid.SelectiveScrollingOrientation="{Binding AreRowDetailsFrozen, ConverterParameter={x:Static SelectiveScrollingOrientation.Vertical}, Converter={x:Static DataGrid.RowDetailsScrollingConverter}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"
                                    Visibility="{TemplateBinding DetailsVisibility}" />
                                <DataGridRowHeader
                                    Grid.RowSpan="2"
                                    SelectiveScrollingGrid.SelectiveScrollingOrientation="Vertical"
                                    Visibility="{Binding HeadersVisibility, ConverterParameter={x:Static DataGridHeadersVisibility.Row}, Converter={x:Static DataGrid.HeadersVisibilityConverter}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" />
                            </SelectiveScrollingGrid>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <Style TargetType="{x:Type DataGrid}">            
            <Setter Property="EnableRowVirtualization" Value="True" />
            <Setter Property="EnableColumnVirtualization" Value="True" />
            <Setter Property="VirtualizingPanel.ScrollUnit" Value="Pixel" />
            <Setter Property="VirtualizingPanel.VirtualizationMode" Value="Standard" />
            <Setter Property="ScrollViewer.CanContentScroll" Value="True" />
        </Style>

        <ControlTemplate x:Key="SimpleExpanderButtonTemp" TargetType="{x:Type ToggleButton}">
            <Border
                x:Name="ExpanderButtonBorder"
                >
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto" />
                        <ColumnDefinition Width="*" />
                    </Grid.ColumnDefinitions>
                    <Rectangle Grid.ColumnSpan="2" Fill="Transparent" />
                    <Ellipse
                        Name="Circle"
                        Grid.Column="0"
                        Width="34"
                        Height="20"
                        HorizontalAlignment="Center"
                        VerticalAlignment="Center"
                        Stroke="Transparent" />
                    <Path
                        x:Name="Sign"
                        Grid.Column="0"
                        Width="10"
                        Height="10"
                        HorizontalAlignment="Center"
                        VerticalAlignment="Center"
                        Data="M 0,5 H 10 M 5,0 V 10 Z"
                        RenderTransformOrigin="0.5,0.5"
                        Stroke="#ADB7C9"
                        StrokeThickness="2">
                        <Path.RenderTransform>
                            <RotateTransform Angle="0" />
                        </Path.RenderTransform>
                    </Path>
                    <ContentPresenter
                        x:Name="HeaderContent"
                        Grid.Column="1"
                        Margin="4,0,0,0"
                        ContentSource="Content" />
                </Grid>
            </Border>
            <ControlTemplate.Triggers>
                <!--  Change the sign to minus when toggled  -->
                <Trigger Property="IsChecked" Value="True">
                    <Setter TargetName="Sign" Property="Data" Value="M 0,5 H 10 Z" />
                </Trigger>

                <!--  MouseOver, Pressed behaviours  -->
                <Trigger Property="IsMouseOver" Value="true">
                    <Setter TargetName="Sign" Property="Stroke" Value="{StaticResource primary-clr}" />
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>

        <!--  Simple Expander Template  -->
        <ControlTemplate x:Key="SimpleExpanderTemp" TargetType="{x:Type Expander}">
            <DockPanel>
                <ToggleButton
                    x:Name="ExpanderButton"
                    Padding="1.5,0"
                    Content="{TemplateBinding Header}"
                    DockPanel.Dock="Top"
                    IsChecked="{Binding Path=IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"
                    OverridesDefaultStyle="True"
                    Template="{StaticResource SimpleExpanderButtonTemp}" />
                <ContentPresenter
                    x:Name="ExpanderContent"
                    DockPanel.Dock="Bottom"
                    Visibility="Collapsed" />
            </DockPanel>
            <ControlTemplate.Triggers>
                <Trigger Property="IsExpanded" Value="True">
                    <Setter TargetName="ExpanderContent" Property="Visibility" Value="Visible" />
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>
    </UserControl.Resources>

    <Grid>
        <DataGrid x:Name="outstandingPatientDataGrid" ItemsSource="{Binding}"  >
            <DataGrid.Columns>
                <DataGridTemplateColumn Width="Auto">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Expander
                                Collapsed="Expander_Collapsed"
                                Expanded="Expander_Expanded"
                                Template="{StaticResource SimpleExpanderTemp}" />
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
                <DataGridTemplateColumn Width="410" CanUserResize="False">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <DockPanel>
                                <Grid>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="*" />
                                        <ColumnDefinition Width="Auto" />
                                    </Grid.ColumnDefinitions>
                                    <TextBlock Grid.Column="0">
                                        <TextBlock.Text>
                                            <MultiBinding StringFormat="{}{0} {1}">
                                                <Binding Path="FirstName" />
                                                <Binding Path="LastName" />
                                            </MultiBinding>
                                        </TextBlock.Text>
                                    </TextBlock>
                                    <TextBlock Grid.Column="1" Text="{Binding Total, StringFormat={}{0:C}}" />
                                </Grid>
                            </DockPanel>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>

            <DataGrid.RowDetailsTemplate>
                <DataTemplate>
                    // ROW STYLES
                </DataTemplate>
            </DataGrid.RowDetailsTemplate>
        </DataGrid>        
    </Grid>
</UserControl>

This is how it looks like: Collapsed:

enter image description here

Expanded:

enter image description here

Code Behind:

using ProjectName.Classes;
using System.Windows;
using System.Windows.Controls;

namespace ProjectName.View.HomePage
{
    public partial class OutstandingPatientDataGrid : UserControl
    {
        public OutstandingPatientDataGrid()
        {
            InitializeComponent();
        }

        private void Expander_Expanded(object sender, RoutedEventArgs e)
        {
            DataGridRow row = TreeHelpers.FindParent<DataGridRow>(sender as Expander);
            row.DetailsVisibility = Visibility.Visible;
        }

        private void Expander_Collapsed(object sender, RoutedEventArgs e)
        {
            DataGridRow row = TreeHelpers.FindParent<DataGridRow>(sender as Expander);
            row.DetailsVisibility = Visibility.Collapsed;
        }
    }
}


Solution

  • So regarding the solution:

    You have two event handlers that set the DetailsVisibility which covers the usecase that the expander togglebutton is toggled by the user. But the other way around is not present: when the DetailsVisibility is set (by the virtualizing system), the togglebutton.IsChecked is not set.

    So instead of using eventhandlers, i would directly bind the IsExpanded to the DetailsVisibility of the row:

      <Expander
        IsExpanded="{Binding DetailsVisibility, RelativeSource={RelativeSource 
          AncestorType=DataGridRow}, Converter={StaticResource 
          VisibilityToBooleanConverter}, Mode=TwoWay, 
          UpdateSourceTrigger=PropertyChanged}"
        Template="{StaticResource SimpleExpanderTemp}" />
    

    The converter should be added to the resources of the UserControl:

    <YourNamespace:VisibilityToBooleanConverter x:Key="VisibilityToBooleanConverter"/>
    

    And the code of the converter would be:

    using System;
    using System.Globalization;
    using System.Windows;
    using System.Windows.Data;
    
    namespace YourNamespace
    {
        public class VisibilityToBooleanConverter : IValueConverter
        {
            public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
            {
                Visibility visibility = (Visibility)value;
                return visibility == Visibility.Visible;
            }
    
            public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
            {
                bool booleanValue = (bool)value;
                return booleanValue ? Visibility.Visible : Visibility.Collapsed;
            }
        }
    }
    

    But apart from that, it is strange that you use an Expander since you don't use the Header and Content of it. All you need is a ToggleButton, so i would replace the Expander by a simple ToggleButton and bind its IsChecked property like the way above.