dotnet / wpf

WPF is a .NET Core UI framework for building Windows desktop applications.
MIT License
7.1k stars 1.17k forks source link

How to freeze last column in WPF datagrid ? #2591

Open Siyumii opened 4 years ago

Siyumii commented 4 years ago

I need to freeze last column of the WPF datagrid. When I set the FrozenColumnCount as 1, then it will freeze only leftmost column but I need to freeze right most column. (that means I need to prevent column from moving). How to achieve this? Seems like need to write custom extension. But I don't have any idea of how to do that? Any guidance or advice will be much appreciated!

SamBent commented 4 years ago

This is probably not possible. The doc says "Frozen columns are always the leftmost columns in display order". There are no hooks into DataGrid's layout that support any other policy.

You can easily change the display order of columns to put the special column at the left, where you can freeze it.

miloush commented 4 years ago

I don't know what your requirements are, but you might be able to fake it by using FlowDirection="RightToLeft" (and fixing the rest with CellStyle etc.). No need to say you break the built-in LTR/RTL support though.

EDIT: To clarify, it will still need to be the first column that is frozen, but it'll be frozen to the right side.

Siyumii commented 4 years ago

Thanks @miloush and @SamBent

Siyumii commented 4 years ago

I tried this alternative solution you have mentioned @miloush . Last column can be freezed.But then so many other issues are rising compared to normal WPF data-grid behavior. Such as when columns are resizing,It resizes to left side but normal grid columns are resizing to right side etc.Anyway thanks for the feedback.

miloush commented 4 years ago

@Siyumii have you thought about having two DataGrids next to each other, with the right one having just the column you want to be fixed?

Siyumii commented 4 years ago

@miloush I hope his approach will not be applicable for my scenario since No of columns displayed in the grid, is not static. In the application currently I'm developing, we can add and remove no of columns to be displayed. So this approach will not be better solution for this scenario.Anyway thanks again for the help !!

miloush commented 4 years ago

@Siyumii You can still do it dynamically, just not in XAML. Quick prototype:

<Window x:Class="FrozenGrid.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="450" Width="800">
    <DockPanel Margin="50">
        <Slider DockPanel.Dock="Bottom" Value="5" Minimum="0" Maximum="6" 
                TickFrequency="1" IsSnapToTickEnabled="True" ValueChanged="OnSliderSlided" />

        <DataGrid DockPanel.Dock="Right" Name="_frozen" BorderThickness="0,1,1,1" AutoGenerateColumns="False" ScrollViewer.ScrollChanged="OnFrozenScrolled" HorizontalScrollBarVisibility="Visible">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding [5]}" Header="F" />
            </DataGrid.Columns>
        </DataGrid>
        <DataGrid Name="_grid" AutoGenerateColumns="False" VerticalScrollBarVisibility="Hidden" ScrollViewer.ScrollChanged="OnGridScrolled">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding [0]}" Header="A" />
                <DataGridTextColumn Binding="{Binding [1]}" Header="B" />
                <DataGridTextColumn Binding="{Binding [2]}" Header="C" />
                <DataGridTextColumn Binding="{Binding [3]}" Header="D" />
                <DataGridTextColumn Binding="{Binding [4]}" Header="E" />
            </DataGrid.Columns>
        </DataGrid>
    </DockPanel>
</Window>
using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace FrozenGrid
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            double[][] data = new double[50][];
            Random random = new Random();
            for (int i = 0; i < 50; i++)
                data[i] = Enumerable.Range(0, 10).Select(r => random.NextDouble()).ToArray();

            _grid.ItemsSource = data;
            _frozen.ItemsSource = data;
        }

        private ScrollViewer _gridScroll;
        private ScrollViewer _frozenScroll;
        private void OnGridScrolled(object sender, ScrollChangedEventArgs e) => OnScrolled(_frozen, ref _frozenScroll, e);
        private void OnFrozenScrolled(object sender, ScrollChangedEventArgs e) => OnScrolled(_grid, ref _gridScroll, e);
        private void OnScrolled(DataGrid targetGrid, ref ScrollViewer targetScroll, ScrollChangedEventArgs e)
        {
            if (targetScroll == null)
                targetScroll = (ScrollViewer)VisualTreeHelper.GetChild(VisualTreeHelper.GetChild(targetGrid, 0), 0);  // Grid > DG_ScrollViewer

            targetScroll.ScrollToVerticalOffset(e.VerticalOffset);
        }

        private void OnSliderSlided(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            if (!IsInitialized)
                return;

            if (e.NewValue > e.OldValue)
            {
                int freeColumns = (int)(e.NewValue - e.OldValue);
                for (int i = 0; i < freeColumns; i++)
                {
                    DataGridColumn c = _frozen.Columns.First();
                    _frozen.Columns.Remove(c);
                    _grid.Columns.Add(c);
                }
            }
            else
            {
                int fixColumns = (int)(e.OldValue - e.NewValue);
                for (int i = 0; i < fixColumns; i++)
                {
                    DataGridColumn c = _grid.Columns.Last();
                    _grid.Columns.Remove(c);
                    _frozen.Columns.Insert(0, c);
                }
            }

            _grid.Visibility = _grid.Columns.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
            _frozen.Visibility = _frozen.Columns.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
            _grid.VerticalScrollBarVisibility = _frozen.Columns.Count < 1 ? ScrollBarVisibility.Visible : ScrollBarVisibility.Hidden;
        }
    }
}

Move the slider to adjust the number of frozen columns. You could turn this into a user control for simple reuse.

legistek commented 3 years ago

@miloush tried your solution - interesting! - but am getting weird behavior. After I scroll (using the mouse wheel) just a bit it jumps to offset 0. Any ideas?

PS - I believe it has to do with row virtualization.

liwuqingxin commented 2 years ago

@Siyumii Hi! you can define a class that derived from DataGridCellsPanel and override the ArrangeOverride method to calculate the arrangement of the last few columns to frozen them. The code is like this.

using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;

namespace Framework.Controls
{
    public class RightFrozenSupportedDataGridCellsPanel : DataGridCellsPanel
    {
        private bool _isVisualUpdated;

        private DataGrid     _parentDataGrid;
        private ScrollViewer _scrollViewer;
        private Style        _frozenColumnCellsStyle;
        private int          _rightFrozenColumnCount;
        private double       _rowHeaderWidth;
        private double       _verticalScrollBarWidth;

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            ObtainVisualElement();
        }

        private bool ObtainVisualElement()
        {
            if (_parentDataGrid == null)
            {
                if ((_parentDataGrid = this.FindVisualParent<DataGrid>()) == null)
                {
                    return false;
                }
            }

            if (_scrollViewer == null)
            {
                if ((_scrollViewer = this.FindVisualParent<ScrollViewer>()) == null)
                {
                    return false;
                }
            }

            _frozenColumnCellsStyle = DataGridExt.GetFrozenColumnCellsStyle(_parentDataGrid);

            return true;
        }

        private void UpdateFrozenColumnsVisual()
        {
            var visualNeedUpdate = DataGridExt.GetIsRightFrozenColumnCountChanged(_parentDataGrid);
            if (visualNeedUpdate == false && _isVisualUpdated)
            {
                return;
            }

            _rightFrozenColumnCount = DataGridExt.GetRightFrozenColumnCount(_parentDataGrid);
            _rowHeaderWidth = (double)TryFindResource(DataGridExt.DataGridRowHeaderWidthKey);

            _verticalScrollBarWidth = SystemParameters.VerticalScrollBarWidth + 2;

            for (var i = 1; i <= _rightFrozenColumnCount; i++)
            {
                var index  = InternalChildren.Count - i;
                var column = _parentDataGrid.Columns[index];
                var child  = InternalChildren[index];

                column.CanUserResize  = false;
                column.CanUserReorder = false;
                if (child is DataGridCell cell && _frozenColumnCellsStyle != null)
                {
                    cell.Style = _frozenColumnCellsStyle;
                }

                if (i == _rightFrozenColumnCount)
                {
                    var control = (Control)child;
                    control.Margin          = new Thickness(-1, 0, 0, 0);
                    if (control is DataGridCell)
                    {
                        control.BorderThickness = new Thickness(1, 0, 0, 0);
                    }
                    else if (control is DataGridColumnHeader)
                    {
                        control.BorderThickness = new Thickness(1, 0, 1, 1);
                    }
                }
            }

            _isVisualUpdated = true;
        }

        protected override Size ArrangeOverride(Size arrangeSize)
        {
            var baseSize = base.ArrangeOverride(arrangeSize);

            TryFrozenRightColumns(arrangeSize);

            return baseSize;
        }

        private void TryFrozenRightColumns(Size arrangeSize)
        {
            if (ObtainVisualElement() == false)
            {
                return;
            }

            UpdateFrozenColumnsVisual();

            if (_rightFrozenColumnCount == 0)
            {
                return;
            }
            if (_parentDataGrid.HeadersVisibility == DataGridHeadersVisibility.Column || _parentDataGrid.HeadersVisibility == DataGridHeadersVisibility.None)
            {
                _rowHeaderWidth = 0;
            }
            if (_scrollViewer.ComputedVerticalScrollBarVisibility == Visibility.Collapsed)
            {
                _verticalScrollBarWidth = 2;
            }
            var horizontalScrollOffset = (double)_parentDataGrid.GetProperty("HorizontalScrollOffset");
            var rowVisualWidth = _parentDataGrid.ActualWidth - _rowHeaderWidth - _verticalScrollBarWidth;
            var offset = rowVisualWidth + horizontalScrollOffset;

            for (var i = 1; i <= _rightFrozenColumnCount; i++)
            {
                var index       = InternalChildren.Count - i;
                var column      = _parentDataGrid.Columns[index];
                var columnWidth = column.Width;
                var child       = InternalChildren[index];

                var point1 = new Point(offset - columnWidth.DisplayValue, 0);
                var point2 = new Point(offset, arrangeSize.Height);

                if (i == _rightFrozenColumnCount)
                {
                    // add 1px offset to the first column to frozen
                    point1 = new Point(offset - columnWidth.DisplayValue - 1, 0);
                }

                child.Arrange(new Rect(point1, point2));

                offset -= columnWidth.DisplayValue;
            }
        }
    }
}

The class DataGridExt is like this.

using System.Windows;
using System.Windows.Controls;

namespace Framework.Controls.Extensions
{
    public static class DataGridExt
    {
        public static readonly DependencyProperty RightFrozenColumnCountProperty = DependencyProperty.RegisterAttached("RightFrozenColumnCount", typeof(int), typeof(DataGridExt), new PropertyMetadata(0, RightFrozenColumnCountChangedCallback));
        private static void RightFrozenColumnCountChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            SetIsRightFrozenColumnCountChanged(d, true);
        }
        [AttachedPropertyBrowsableForType(typeof(DataGrid))]
        public static int GetRightFrozenColumnCount(DependencyObject obj)
        {
            return (int)obj.GetValue(RightFrozenColumnCountProperty);
        }
        public static void SetRightFrozenColumnCount(DependencyObject obj, int value)
        {
            obj.SetValue(RightFrozenColumnCountProperty, value);
        }

        internal static readonly DependencyProperty IsRightFrozenColumnCountChangedProperty = DependencyProperty.RegisterAttached("IsRightFrozenColumnCountChanged", typeof(bool), typeof(DataGridExt), new PropertyMetadata(default));
        internal static bool GetIsRightFrozenColumnCountChanged(DependencyObject obj)
        {
            return (bool)obj.GetValue(IsRightFrozenColumnCountChangedProperty);
        }
        private static void SetIsRightFrozenColumnCountChanged(DependencyObject obj, bool value)
        {
            obj.SetValue(IsRightFrozenColumnCountChangedProperty, value);
        }

        public static readonly DependencyProperty FrozenColumnCellsStyleProperty = DependencyProperty.RegisterAttached("FrozenColumnCellsStyle", typeof(Style), typeof(DataGridExt), new PropertyMetadata(default));
        [AttachedPropertyBrowsableForType(typeof(DataGrid))]
        public static Style GetFrozenColumnCellsStyle(DependencyObject obj)
        {
            return (Style)obj.GetValue(FrozenColumnCellsStyleProperty);
        }
        public static void SetFrozenColumnCellsStyle(DependencyObject obj, Style value)
        {
            obj.SetValue(FrozenColumnCellsStyleProperty, value);
        }

        public static ResourceKey DataGridRowHeaderWidthKey { get; } = new ComponentResourceKey(typeof(DataGridExt), "DataGridRowHeaderWidth");
    }
}

And then, you should re-define the template of DataGrid and change the ItemsPanel of DataGridRow and DataGridColumnHeaderPresenter to the RightFrozenSupportedDataGridCellsPanel.

liwuqingxin commented 2 years ago

LOOK HERE.

004

legistek commented 2 years ago

Love it! The option to subclass WPF controls, and especially their component parts, is too often overlooked. A shame so much of UWP/WinUI is sealed.

hwybao commented 2 years ago

DataGridColumnHeaderPresenter

Hi, can you upload a demo with source code? thank u : )

liwuqingxin commented 2 years ago

"Hi, can you upload a demo with source code? thank u : )"

Sorry @hwybao. I can only offer the code of xaml. You should focus on the usings of class RightFrozenSupportedDataGridCellsPanel. I hope it could help.

    <Style x:Key="DataGridRowStyle" TargetType="{x:Type DataGridRow}">
        <Setter Property="Height" Value="{DynamicResource DataGrid.Row.Height}"/>
        <Setter Property="VerticalContentAlignment" Value="Center"/>
        <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
        <Setter Property="Foreground" Value="{DynamicResource DataGrid.Row.Foreground}" />
        <Setter Property="Background" Value="{DynamicResource DataGrid.Row.Background}" />
        <Setter Property="BorderBrush" Value="{DynamicResource DataGrid.BorderBrush}"/>
        <Setter Property="BorderThickness" Value="0,0,1,0" />
        <Setter Property="ItemsPanel">
            <Setter.Value>
                <ItemsPanelTemplate>
                    <controls:RightFrozenSupportedDataGridCellsPanel></controls:RightFrozenSupportedDataGridCellsPanel>
                </ItemsPanelTemplate>
            </Setter.Value>
        </Setter>
        <Style.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter Property="Foreground" Value="{DynamicResource DataGrid.Row.Foreground.Hover}"/>
                <Setter Property="Background" Value="{DynamicResource DataGrid.Row.Background.Hover}"/>
            </Trigger>
            <Trigger Property="IsSelected" Value="True">
                <Setter Property="Foreground" Value="{DynamicResource DataGrid.Row.Foreground.Selected}"/>
                <Setter Property="Background" Value="{DynamicResource DataGrid.Row.Background.Selected}"/>
            </Trigger>
        </Style.Triggers>
    </Style>

<ControlTemplate x:Key="DataGridScrollViewerTemplate" TargetType="{x:Type ScrollViewer}">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>

            <!-- 全选按钮:0,0 -->
            <controls:DataGridSelectAllButton Style="{StaticResource {x:Type CheckBox}}"
                                              Width="{DynamicResource {x:Static extensions:DataGridExt.DataGridRowHeaderWidthKey}}"
                                              Background="{DynamicResource DataGrid.Header.Background}"
                                              Foreground="{DynamicResource DataGrid.Header.Foreground}"
                                              BorderBrush="{DynamicResource DataGrid.BorderBrush}"
                                              BorderThickness="0,0,1,1"
                                              IsThreeState="True"
                                              HorizontalContentAlignment="Center"
                                              HorizontalAlignment="Stretch"
                                              VerticalAlignment="Stretch"
                                              Focusable="false"
                                              Visibility="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}, Path=HeadersVisibility, Converter={x:Static DataGrid.HeadersVisibilityConverter}, ConverterParameter={x:Static DataGridHeadersVisibility.All}}" />

            <!-- 列标题栏:0,1 -->
            <DataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter"
                                            Grid.Row="0" Grid.Column="1"
                                            Visibility="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}, Path=HeadersVisibility, Converter={x:Static DataGrid.HeadersVisibilityConverter}, ConverterParameter={x:Static DataGridHeadersVisibility.Column}}">
                <DataGridColumnHeadersPresenter.ItemsPanel>
                    <ItemsPanelTemplate>
                        <controls:RightFrozenSupportedDataGridCellsPanel></controls:RightFrozenSupportedDataGridCellsPanel>
                    </ItemsPanelTemplate>
                </DataGridColumnHeadersPresenter.ItemsPanel>
            </DataGridColumnHeadersPresenter>

            <!-- 列标题与垂直滚动条夹角的空白区域:0,3 -->
            <Border Grid.Row="0" Grid.Column="3" 
                                 Background="{DynamicResource DataGrid.Header.Background}"
                                 BorderBrush="{DynamicResource DataGrid.BorderBrush}"
                                 BorderThickness="1,0,0,1"
                                 Margin="-1,0,0,0"/>

            <!-- 滚动内容区域:1,0(2) -->
            <ScrollContentPresenter x:Name="PART_ScrollContentPresenter" 
                                    Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
                                    CanContentScroll="{TemplateBinding CanContentScroll}" />

            <!-- 垂直滚动条:1,3 -->
            <ScrollBar Grid.Row="1" Grid.Column="3" Name="PART_VerticalScrollBar"
                                    Orientation="Vertical"
                                    Maximum="{TemplateBinding ScrollableHeight}"
                                    ViewportSize="{TemplateBinding ViewportHeight}"
                                    Value="{Binding Path=VerticalOffset, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"
                                    Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"/>

            <!-- 水平滚动条:2,1 -->
            <Grid Grid.Row="2" Grid.Column="1">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}, Path=NonFrozenColumnsViewportHorizontalOffset}"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <ScrollBar Grid.Column="1"
                           Name="PART_HorizontalScrollBar"
                           Orientation="Horizontal"
                           Maximum="{TemplateBinding ScrollableWidth}"
                           ViewportSize="{TemplateBinding ViewportWidth}"
                           Value="{Binding Path=HorizontalOffset, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"
                           Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"/>
            </Grid>
        </Grid>
    </ControlTemplate>
hwybao commented 2 years ago

"Hi, can you upload a demo with source code? thank u : )"

Sorry @hwybao. I can only offer the code of xaml. You should focus on the usings of class RightFrozenSupportedDataGridCellsPanel. I hope it could help.

    <Style x:Key="DataGridRowStyle" TargetType="{x:Type DataGridRow}">
        <Setter Property="Height" Value="{DynamicResource DataGrid.Row.Height}"/>
        <Setter Property="VerticalContentAlignment" Value="Center"/>
        <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
        <Setter Property="Foreground" Value="{DynamicResource DataGrid.Row.Foreground}" />
        <Setter Property="Background" Value="{DynamicResource DataGrid.Row.Background}" />
        <Setter Property="BorderBrush" Value="{DynamicResource DataGrid.BorderBrush}"/>
        <Setter Property="BorderThickness" Value="0,0,1,0" />
        <Setter Property="ItemsPanel">
            <Setter.Value>
                <ItemsPanelTemplate>
                    <controls:RightFrozenSupportedDataGridCellsPanel></controls:RightFrozenSupportedDataGridCellsPanel>
                </ItemsPanelTemplate>
            </Setter.Value>
        </Setter>
        <Style.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter Property="Foreground" Value="{DynamicResource DataGrid.Row.Foreground.Hover}"/>
                <Setter Property="Background" Value="{DynamicResource DataGrid.Row.Background.Hover}"/>
            </Trigger>
            <Trigger Property="IsSelected" Value="True">
                <Setter Property="Foreground" Value="{DynamicResource DataGrid.Row.Foreground.Selected}"/>
                <Setter Property="Background" Value="{DynamicResource DataGrid.Row.Background.Selected}"/>
            </Trigger>
        </Style.Triggers>
    </Style>

<ControlTemplate x:Key="DataGridScrollViewerTemplate" TargetType="{x:Type ScrollViewer}">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>

            <!-- 全选按钮:0,0 -->
            <controls:DataGridSelectAllButton Style="{StaticResource {x:Type CheckBox}}"
                                              Width="{DynamicResource {x:Static extensions:DataGridExt.DataGridRowHeaderWidthKey}}"
                                              Background="{DynamicResource DataGrid.Header.Background}"
                                              Foreground="{DynamicResource DataGrid.Header.Foreground}"
                                              BorderBrush="{DynamicResource DataGrid.BorderBrush}"
                                              BorderThickness="0,0,1,1"
                                              IsThreeState="True"
                                              HorizontalContentAlignment="Center"
                                              HorizontalAlignment="Stretch"
                                              VerticalAlignment="Stretch"
                                              Focusable="false"
                                              Visibility="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}, Path=HeadersVisibility, Converter={x:Static DataGrid.HeadersVisibilityConverter}, ConverterParameter={x:Static DataGridHeadersVisibility.All}}" />

            <!-- 列标题栏:0,1 -->
            <DataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter"
                                            Grid.Row="0" Grid.Column="1"
                                            Visibility="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}, Path=HeadersVisibility, Converter={x:Static DataGrid.HeadersVisibilityConverter}, ConverterParameter={x:Static DataGridHeadersVisibility.Column}}">
                <DataGridColumnHeadersPresenter.ItemsPanel>
                    <ItemsPanelTemplate>
                        <controls:RightFrozenSupportedDataGridCellsPanel></controls:RightFrozenSupportedDataGridCellsPanel>
                    </ItemsPanelTemplate>
                </DataGridColumnHeadersPresenter.ItemsPanel>
            </DataGridColumnHeadersPresenter>

            <!-- 列标题与垂直滚动条夹角的空白区域:0,3 -->
            <Border Grid.Row="0" Grid.Column="3" 
                                 Background="{DynamicResource DataGrid.Header.Background}"
                                 BorderBrush="{DynamicResource DataGrid.BorderBrush}"
                                 BorderThickness="1,0,0,1"
                                 Margin="-1,0,0,0"/>

            <!-- 滚动内容区域:1,0(2) -->
            <ScrollContentPresenter x:Name="PART_ScrollContentPresenter" 
                                    Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
                                    CanContentScroll="{TemplateBinding CanContentScroll}" />

            <!-- 垂直滚动条:1,3 -->
            <ScrollBar Grid.Row="1" Grid.Column="3" Name="PART_VerticalScrollBar"
                                    Orientation="Vertical"
                                    Maximum="{TemplateBinding ScrollableHeight}"
                                    ViewportSize="{TemplateBinding ViewportHeight}"
                                    Value="{Binding Path=VerticalOffset, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"
                                    Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"/>

            <!-- 水平滚动条:2,1 -->
            <Grid Grid.Row="2" Grid.Column="1">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}, Path=NonFrozenColumnsViewportHorizontalOffset}"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <ScrollBar Grid.Column="1"
                           Name="PART_HorizontalScrollBar"
                           Orientation="Horizontal"
                           Maximum="{TemplateBinding ScrollableWidth}"
                           ViewportSize="{TemplateBinding ViewportWidth}"
                           Value="{Binding Path=HorizontalOffset, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"
                           Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"/>
            </Grid>
        </Grid>
    </ControlTemplate>

Thank you so much!!! @liwuqingxin I implemented this function by referring to your code.

LeMinhDong commented 2 years ago

LOOK HERE.

004

Can you give me the project please? thank you

liwuqingxin commented 2 years ago

Can you give me the project please? thank you

@LeminhDong, sorry for that I don't have a clean demo project for it. All related code about it has been shown above. If you have any problems about it I am glad to discuss them with you 😄.

miloush commented 2 years ago

While there has been some good workarounds posted in this thread, they are not trivial and I think it might be worth considering some in-box support, such as allowing negative FreezeColumnCount values or some kind of FreezeDirection property.

vikasjain6 commented 2 years ago

@liwuqingxin, i copied your code and try to build, but build is failing due to following things, kindly help if possible.

  1. this.FindVisualParent())
  2. var horizontalScrollOffset = (double)_parentDataGrid.GetProperty("HorizontalScrollOffset");
  3. <controls:DataGridSelectAllButton

kindly prepare one simple DataGrid Project and share here.

Can you give me the project please? thank you

@LeMinhDong, sorry for that I don't have a clean demo project for it. All related code about it has been shown above. If you have any problems about it I am glad to discuss them with you 😄.

liwuqingxin commented 2 years ago

@vikasjain6 @LeMinhDong DataGrid.RightFrozen.Demo by NLNet 2022.10.13.zip is a simple dmeo of that.

vikasjain6 commented 2 years ago

@liwuqingxin thanks a lot for great sample application

@vikasjain6 @LeMinhDong DataGrid.RightFrozen.Demo by NLNet 2022.10.13.zip is a simple dmeo of that.

PrinceOwen9466 commented 1 week ago

While everyone is still waiting for an out of the box solution, I found the solution from miloush to be pretty clever. I wrote a small Behavior for anyone who would like to use this quick workaround.

ReactiveExtensions.cs

/// <summary>
/// Subscribes to events using reflection. You're welcome :)
/// </summary>
public static class ReactiveExtensions
{
    private static EventHandler<TEvent> CreateGenericHandler<TEvent>(object target, MethodInfo method)
    {
        return (EventHandler<TEvent>)Delegate.CreateDelegate(typeof(EventHandler<TEvent>),
            target, method);
    }

    private static EventHandler CreateHandler(object target, MethodInfo method)
    {
        return (EventHandler)Delegate.CreateDelegate(typeof(EventHandler),
            target, method);
    }

    private static RoutedEventHandler CreateRoutedHandler(object target, MethodInfo method)
    {
        return (RoutedEventHandler)Delegate.CreateDelegate(typeof(RoutedEventHandler),
            target, method);
    }

    public static Delegate BindEventToAction(this EventInfo eventInfo, object target, Delegate action)
    {
        MethodInfo method;

        if (eventInfo.EventHandlerType.IsGenericType)
        {
            method = typeof(ReactiveExtensions)
                .GetMethod(nameof(CreateGenericHandler))
                .MakeGenericMethod(
                eventInfo.EventHandlerType.GetGenericArguments());
        }
        else if (eventInfo.EventHandlerType == typeof(RoutedEventHandler))
        {
            method = typeof(ReactiveExtensions).GetMethod(nameof(CreateRoutedHandler));
        }
        else
        {
            method = typeof(ReactiveExtensions).GetMethod(nameof(CreateHandler));
        }

        Delegate @delegate = (Delegate)method.Invoke(null, new object[] { action.Target, action.Method });
        eventInfo.AddEventHandler(target, @delegate);

        return @delegate;
    }

    public static void SubscribeOrSkip<T>(this T entity, string eventName, Predicate<T> skipCondition, Action handler = null)
    {
        if (skipCondition(entity))
        {
            handler?.Invoke();
            return;
        }

        Delegate @delegate = null;
        EventInfo eventInfo = typeof(T).GetEvent(eventName);

        if (eventInfo == null)
            throw new InvalidOperationException($"Failed to subscribe to {eventName} event on type {typeof(T).Name}");

        Action<object, object> action = (s, e) =>
        {
            eventInfo.RemoveEventHandler(entity, @delegate);
            handler?.Invoke();
        };

        @delegate = BindEventToAction(eventInfo, entity, action);
    }
}

MirrorScrollBehavior.cs

 public class MirrorScrollBehavior : Behavior<ItemsControl>
 {
     private ScrollViewer _targetScroll;
     private ScrollViewer _associatedScroll;

     public ItemsControl Target
     {
         get { return (ItemsControl)GetValue(TargetProperty); }
         set { SetValue(TargetProperty, value); }
     }

     public static readonly DependencyProperty TargetProperty =
         DependencyProperty.Register("Target", typeof(ItemsControl), typeof(MirrorScrollBehavior), new PropertyMetadata(null, HandleTargetChanged));

     private static void HandleTargetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
     {
         if (d is not MirrorScrollBehavior beh) return;
         beh.HandleTargetChangedInternal(e);
     }

     private void HandleTargetChangedInternal(DependencyPropertyChangedEventArgs e)
     {
         if (_targetScroll != null)
         {
             _targetScroll.ScrollChanged -= HandleTargetScroll;
         }

         if (e.NewValue is not ItemsControl target) return;

         // Ensures that the ItemsControl has been loaded
         target.SubscribeOrSkip(nameof(ItemsControl.Loaded), (x) => x.IsLoaded, () =>
         {
             _targetScroll = UIHelper.FindVisualChildren<ScrollViewer>(target).FirstOrDefault();

             if (_targetScroll != null)
             {
                 _targetScroll.ScrollChanged += HandleTargetScroll;

                 if (_associatedScroll != null)
                     _targetScroll.ScrollToVerticalOffset(_associatedScroll.VerticalOffset);
             }
         });
     }

     protected override void OnAttached()
     {
         ItemsControl associated = AssociatedObject;

         // Ensures that the ItemsControl has been loaded
         associated.SubscribeOrSkip(nameof(ItemsControl.Loaded), (x) => x.IsLoaded, () =>
         {
             _associatedScroll = UIHelper.FindVisualChildren<ScrollViewer>(associated).FirstOrDefault();
             if (_associatedScroll != null)
             {
                 _associatedScroll.ScrollChanged += HandleAssociatedScroll;
             }
         });

         base.OnAttached();
     }

     protected override void OnDetaching()
     {
         if (_associatedScroll != null)
         {
             _associatedScroll.ScrollChanged -= HandleAssociatedScroll;
         }

         if (_targetScroll != null)
         {
             _targetScroll.ScrollChanged -= HandleAssociatedScroll;
         }

         _associatedScroll = _targetScroll = null;

         base.OnDetaching();
     }

     private void HandleAssociatedScroll(object sender, ScrollChangedEventArgs e)
     {
         if (_targetScroll == null) return;
         if (_targetScroll.VerticalOffset == e.VerticalOffset) return;

         _targetScroll.ScrollToVerticalOffset(e.VerticalOffset);
     }

     private void HandleTargetScroll(object sender, ScrollChangedEventArgs e)
     {
         if (_associatedScroll == null) return;
         if (_associatedScroll.VerticalOffset == e.VerticalOffset) return;

         _associatedScroll.ScrollToVerticalOffset(e.VerticalOffset);
     }
 }

Now your ListBox or DataGrid will always scroll together

 <UserControl
     xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
     >
     <DockPanel>
         <DataGrid Name="frozenGrid" DockPanel.Dock="Right">

         </DataGrid>

         <DataGrid SelectedItem="{Binding ElementName=frozenGrid, Path=SelectedItem, Mode=TwoWay}"
                           SelectionMode="Extended" SelectionUnit="FullRow">
             <i:Interaction.Behaviors>
                 <ui:MirrorScrollBehavior Target="{Binding ElementName=frozenGrid}" />
             </i:Interaction.Behaviors>
         </DataGrid>
     </DockPanel>
 </UserControl>

Note: RowHeight will have to be fixed for both controls