scrolling problem with combobox inside datagrid inside scrollviewer

39 Views Asked by At

The UI is working, meaning the data looks good, however, we have a scrolling problem.

The data is in a series of ComboBoxes, inside of a DataGrid because we have many ComboBoxes, which is inside a ScrollViewer because the series is too long to fit the small window.

What I found out is that the DataGrid does not scroll inside the ScrollViewer, but with SO answer this is easily fixed: add a handler for the PreviewMouseWheel event for the DataGrid that forwards (bubbles) that event to the ScrollViewer.

Now the problem moves to the ComboBoxes. When we click the ComboBox and see the ComboBoxListItems, the scrolling events reaches the ScrollViewer, probably through the added event handler. As an effect, when I try to scroll the ComboBox Item List, this has only limited effect, I can only scroll one item higher or lower. Plus, the ScrollViewer also scrolls.

To be precise, the problem refers to the scroll wheel. Scrolling by dragging the thumb in the scrollbar works perfectly. However, our users have mice with scroll wheel, and obviously expect the scroll wheel to scroll the content that the mouse is hovering over.

The obvious solution would be to add a similar event handler to the ComboBox, to handle the MouseWheel Scroll event, and to mark the event as handled with e.Handled=True .

However, I have no idea how to "handle" the ComboBox scroll event. If I just capture the event and mark it with e.Handled=True, then scrolling the combobox will not work. I found several posts on how to disable scrolling, but nothing with an example for how to handle scrolling in the ComboBox.

Here is the full source code of my small test app:

The XAML:

<ScrollViewer x:Name="theScrollViewer">
    <StackPanel HorizontalAlignment="Stretch">
        <TextBox Text="Test the Scrollwheel in the Palette" FontWeight="Bold" />
        <TextBox Text="Row 1"/>
        <TextBox Text="Row 2"/>
        <TextBox Text="Row 3"/>
        <TextBox Text="Row 4"/>
        <TextBox Text="Row 5"/>
        <TextBox Text="Row 6"/>
        <TextBox Text="Row 7"/>
        <TextBox Text="Row 8"/>
        <TextBox Text="Row 9"/>
        <TextBox Text="Row 10"/>
        <DataGrid Name="dgProperties" ItemsSource="{Binding Value}"
                  PreviewMouseWheel="theDataGrid_PreviewMouseWheel"
                  AutoGenerateColumns="False"
                  ColumnWidth="*"
                  HeadersVisibility="None"
                  >
            <DataGrid.Columns>
                <!-- Left Column -->
                <DataGridTextColumn Binding="{Binding Path=Label}" />
                <!-- Right Column -->
                <DataGridTextColumn Binding="{Binding Path=DisplayValue}" />
                <!-- Third column with ComboBoxes https://stackoverflow.com/a/11942357/1845672 -->
                <DataGridTemplateColumn>
                    <DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <!--inline template, but could also be from Resources-->
                            <ComboBox
                                Text="{Binding Path=DisplayValue}"
                                PreviewMouseWheel="theComboBox_PreviewMouseWheel"
                                >
                                <ComboBoxItem Content="{Binding Path=DisplayValue}" />
                                <ComboBoxItem>Eersel</ComboBoxItem>
                                <ComboBoxItem>Tweede Jan Steenstraat</ComboBoxItem>
                                <ComboBoxItem>Drievliet</ComboBoxItem>
                                <ComboBoxItem>Vierhouten</ComboBoxItem>
                                <ComboBoxItem>Vijfhuizen</ComboBoxItem>
                                <ComboBoxItem>Zestienhoven</ComboBoxItem>
                                <ComboBoxItem>Zevenaar</ComboBoxItem>
                                <ComboBoxItem>Achterhoek</ComboBoxItem>
                                <ComboBoxItem>Negen Straatjes</ComboBoxItem>
                                <ComboBoxItem>Tiengemeten</ComboBoxItem>
                                <ComboBoxItem>Elfstedenhal</ComboBoxItem>
                                <ComboBoxItem>Twaalf Ambachten</ComboBoxItem>
                                <ComboBoxItem>Dertienhuizen</ComboBoxItem>
                                <ComboBoxItem>Amsterdam</ComboBoxItem>
                                <ComboBoxItem>Haarlem</ComboBoxItem>
                                <ComboBoxItem>Den Haag</ComboBoxItem>
                                <ComboBoxItem>Echt</ComboBoxItem>
                                <ComboBoxItem>Budel</ComboBoxItem>
                                <ComboBoxItem>Oosterhout</ComboBoxItem>
                            </ComboBox>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellEditingTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>
    </StackPanel>
</ScrollViewer>
                                >

The code behind:

    public partial class WindowTestScrolllPalette : Window
{
    private ObjectProperty OP = new ObjectProperty();

    public WindowTestScrolllPalette()
    {
        InitializeComponent();
        dgProperties.DataContext = this;
    }

    /// <summary>
    /// Property version of ItemsSource,
    /// for Binding to control.ItemsSource.
    /// </summary>
    public ObservableCollection<ObjectProperty> Value
    {
        get { 
            return OP.GetRows("pipe");
        }
        set { return; }
    }

    /// <summary>
    /// The original scroll event handler, on the ScrollViewer,
    /// to make the DataGrid scrollable
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void theScrollViewer_PreviewMouseWheel_OBSOLETE(object sender, MouseWheelEventArgs e)
    {
        if (sender is ScrollViewer scrollView)
        {
            scrollView.UpdateLayout();
            scrollView.ScrollToVerticalOffset(scrollView.VerticalOffset - e.Delta);
            e.Handled = true;
        }            
    }

    /// <summary>
    /// The new (better) scroll event handler, on the DataGrid,
    /// to make the DataGrid scrollable
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void theDataGrid_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
    {
        // Source:
        // Q https://stackoverflow.com/questions/78194768/wpf-datagrid-does-not-bubble-the-scrollwheel-event
        // @ https://stackoverflow.com/questions/41140287/horizontal-scroll-for-stackpanel-doesnt-work
        // A https://stackoverflow.com/a/61895098/1845672

        //what we're doing here, is that we're invoking the "MouseWheel" event of the parent ScrollViewer.

        //first, we make the object with the event arguments (using the values from the current event)
        var args = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);

        //then we need to set the event that we're invoking.
        //the ScrollViewer control internally does the scrolling on MouseWheelEvent, so that's what we're going to use:
        args.RoutedEvent = ScrollViewer.MouseWheelEvent;

        //and finally, we raise the event on the parent ScrollViewer.
        theScrollViewer.RaiseEvent(args);
    }

    /// <summary>
    /// The hypothetical scroll event handler, on the ComboBox,
    /// to make the ComboBox scrollable
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void theComboBox_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
    {
        if (sender is ComboBox cb)
        {
            var args = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
            args.RoutedEvent = ScrollViewer.MouseWheelEvent;
            //cb.ItemsPanel.RaiseEvents(args);//cb has no such property
            //cb.ScrollToVerticalOffset(cb.VerticalOffset - e.Delta);//cb has no such property
            //e.Handled = true;
        }
    }
}

and here is how I make test data:

public class ObjectProperty : INotifyPropertyChanged
{
    /// <summary>
    /// Content for the left column of the DataGrid
    /// </summary>
    public string Label { get; set; }

    /// <summary>
    /// Content for the right column of the DataGrid
    /// </summary>
    public string DisplayValue { get; set; }

    /// <summary>
    /// Raised event when a property changes.
    /// This handler is required for classes derived from INotifyPropertyChanged.
    /// If this handler is non-null, it will be invoked by OnPropertyChanged (below).
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// Method called in the 'setter' of properties whom changes have to be notified.
    /// </summary>
    /// <param name="propertyName">Property name.</param>
    protected void OnPropertyChanged(string propertyName) =>  // NOTABENE: LAMBDA NOTATION, BUT IS THIS STILL JUST A REGULAR METHOD???
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

    /// <summary>
    /// Return the list of items for the datagrid
    /// </summary>
    /// <param name="ObjectId"></param>
    /// <returns></returns>
    public ObservableCollection<ObjectProperty> GetRows(string ObjectId)
    {
        // Set up the collection properties used to bind the ItemsSource 
        // properties to display the list of items in the dropdown lists.
        string now = DateTime.Now.ToString("HH:mm:ss.ffffff");
        var L = new ObservableCollection<ObjectProperty>();
        switch (ObjectId.ToLower())
        {
            case "pipe":
                L.Add(new ObjectProperty() { Label = "Length", DisplayValue = "26.3 m" });
                L.Add(new ObjectProperty() { Label = "Material", DisplayValue = "PVC" });
                L.Add(new ObjectProperty() { Label = "Substance", DisplayValue = "Tar" });
                L.Add(new ObjectProperty() { Label = "Address", DisplayValue = "Mainstreet" });
                L.Add(new ObjectProperty() { Label = "ObjectId", DisplayValue = ObjectId });
                L.Add(new ObjectProperty() { Label = "Length", DisplayValue = ObjectId.Length.ToString() });
                L.Add(new ObjectProperty() { Label = "Time", DisplayValue = now });
                L.Add(new ObjectProperty() { Label = "Width", DisplayValue = "0.15 m" });
                L.Add(new ObjectProperty() { Label = "Weight", DisplayValue = "16.2 kg" });
                L.Add(new ObjectProperty() { Label = "Orientation", DisplayValue = "Northwest" });
                L.Add(new ObjectProperty() { Label = "Flexible", DisplayValue = "Yes" });
                L.Add(new ObjectProperty() { Label = "Status", DisplayValue = "Half" });
                break;

            case "line":
            default:
                L.Add(new ObjectProperty() { Label = "Length", DisplayValue = "1.23 m" });
                L.Add(new ObjectProperty() { Label = "Material", DisplayValue = "PVC" });
                L.Add(new ObjectProperty() { Label = "Substance", DisplayValue = "Tar" });
                L.Add(new ObjectProperty() { Label = "Address", DisplayValue = "Backstreet" });
                L.Add(new ObjectProperty() { Label = "ObjectId", DisplayValue = ObjectId });
                L.Add(new ObjectProperty() { Label = "Length", DisplayValue = ObjectId.Length.ToString() });
                L.Add(new ObjectProperty() { Label = "Time", DisplayValue = now });
                L.Add(new ObjectProperty() { Label = "Width", DisplayValue = "0.15 m" });
                L.Add(new ObjectProperty() { Label = "Weight", DisplayValue = "16.2 kg" });
                L.Add(new ObjectProperty() { Label = "Orientation", DisplayValue = "Northwest" });
                L.Add(new ObjectProperty() { Label = "Flexible", DisplayValue = "Yes" });
                L.Add(new ObjectProperty() { Label = "Status", DisplayValue = "Half" });
                break;
        };
        return L;
    }

    /// <summary>
    /// Return the list of items for the datagrid as a Key Values Pair
    /// </summary>
    /// <param name="ObjectId"></param>
    /// <returns></returns>
    public KeyValuePair<string, ObservableCollection<ObjectProperty>> GetRows_KVP(string ObjectId)
    {
        var items = GetRows(ObjectId);
        var itemsKvp = new KeyValuePair<string, ObservableCollection<ObjectProperty>>(ObjectId, items);
        return itemsKvp;
    }

    /// <summary>
    /// The entire dataset for display in the UserControl
    /// </summary>
    /// <returns></returns>
    public ObservableCollection<KeyValuePair<string, ObservableCollection<ObjectProperty>>> TheItemsSource()
    {
        var itemsSource = new ObservableCollection<KeyValuePair<string, ObservableCollection<ObjectProperty>>>();
        itemsSource.Add(GetRows_KVP("Pipe"));
        itemsSource.Add(GetRows_KVP("Line"));
        return itemsSource;
    }

    /// <summary>
    /// Property version of ItemsSource,
    /// for assigning to control.ItemsSource.
    /// </summary>
    public ObservableCollection<KeyValuePair<string, ObservableCollection<ObjectProperty>>> TheItemsSourceProperty {
        get { return TheItemsSource(); }
        set { return; }
    }

}

Here is the screenshot: with the mouse hovering over, e.g., "Negen Straatjes", the scrollwheel will fully scroll the DataGrid, while the ComboBox item list only "scrolls" one item, at max between "Negen Straatjes" and "Tiengemeten":

screenshot of ComboBox inside DataGrid inside ScrollViewer

As for the DataGrid Height: in the above sources I tried in the XAML to add a Height of 400, and removed both PreviewMouseWheel events from DataGrid and ComboBox; the result was that the scroll wheel only worked over the TextBox rows, not over the DataGrid. So the DataGrid definitely needs the event handler.

As a reference, here is another test app, really simple, with no special code in code-behind. The scrolling works on all levels, however, it is a bit funny how the focus moves between ScrollViewer and ComboBox, e.g. by tabbing between input controls, but I guess it is all very logical:

<Window x:Class="xaml_test.Palettes.Views.WindowTestScrollwheel"
    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:xaml_test.Palettes.Views"
    mc:Ignorable="d"
    Title="WindowTestScrollwheel" Width="350" Height="250" >
<ScrollViewer>
    <StackPanel HorizontalAlignment="Stretch">
        <TextBox Text="Test the Scrollwheel" FontWeight="Bold" />
        <TextBox Text="Row 1"/>
        <TextBox Text="Row 2"/>
        <TextBox Text="Row 3"/>
        <TextBox Text="Row 4"/>
        <TextBox Text="Row 5"/>
        <TextBox Text="Row 6"/>
        <TextBox Text="Row 7"/>
        <TextBox Text="Row 8"/>
        <TextBox Text="Row 9"/>
        <TextBox Text="Row 10"/>
        <ComboBox Text="City?" MaxDropDownHeight="150">
            <ComboBoxItem>Eersel</ComboBoxItem>
            <ComboBoxItem>Tweede Jan Steenstraat</ComboBoxItem>
            <ComboBoxItem>Drievliet</ComboBoxItem>
            <ComboBoxItem>Vierhouten</ComboBoxItem>
            <ComboBoxItem>Vijfhuizen</ComboBoxItem>
            <ComboBoxItem>Zestienhoven</ComboBoxItem>
            <ComboBoxItem>Zevenaar</ComboBoxItem>
            <ComboBoxItem>Achterhoek</ComboBoxItem>
            <ComboBoxItem>Negen Straatjes</ComboBoxItem>
            <ComboBoxItem>Tiengemeten</ComboBoxItem>
            <ComboBoxItem>Elfstedenhal</ComboBoxItem>
            <ComboBoxItem>Twaalf Ambachten</ComboBoxItem>
            <ComboBoxItem>Dertienhuizen</ComboBoxItem>
            <ComboBoxItem>Amsterdam</ComboBoxItem>
            <ComboBoxItem>Haarlem</ComboBoxItem>
            <ComboBoxItem>Den Haag</ComboBoxItem>
            <ComboBoxItem>Echt</ComboBoxItem>
            <ComboBoxItem>Budel</ComboBoxItem>
            <ComboBoxItem>Oosterhout</ComboBoxItem>
        </ComboBox>
        <ComboBox Text="cb 2">
            <ComboBoxItem>One</ComboBoxItem>
            <ComboBoxItem>Two</ComboBoxItem>
            <ComboBoxItem>Three</ComboBoxItem>
        </ComboBox>
        <ComboBox Text="cb 3">
            <ComboBoxItem>One</ComboBoxItem>
            <ComboBoxItem>Two</ComboBoxItem>
            <ComboBoxItem>Three</ComboBoxItem>
        </ComboBox>
        <ComboBox Text="cb 4">
            <ComboBoxItem>One</ComboBoxItem>
            <ComboBoxItem>Two</ComboBoxItem>
            <ComboBoxItem>Three</ComboBoxItem>
        </ComboBox>
        <ComboBox Text="cb 5">
            <ComboBoxItem>One</ComboBoxItem>
            <ComboBoxItem>Two</ComboBoxItem>
            <ComboBoxItem>Three</ComboBoxItem>
        </ComboBox>
        <ComboBox Text="cb 6">
            <ComboBoxItem>One</ComboBoxItem>
            <ComboBoxItem>Two</ComboBoxItem>
            <ComboBoxItem>Three</ComboBoxItem>
        </ComboBox>
        <ComboBox Text="cb 7">
            <ComboBoxItem>One</ComboBoxItem>
            <ComboBoxItem>Two</ComboBoxItem>
            <ComboBoxItem>Three</ComboBoxItem>
        </ComboBox>
        <ComboBox Text="cb 8">
            <ComboBoxItem>One</ComboBoxItem>
            <ComboBoxItem>Two</ComboBoxItem>
            <ComboBoxItem>Three</ComboBoxItem>
        </ComboBox>
        <ComboBox Text="cb 9">
            <ComboBoxItem>One</ComboBoxItem>
            <ComboBoxItem>Two</ComboBoxItem>
            <ComboBoxItem>Three</ComboBoxItem>
        </ComboBox>
        <ComboBox Text="cb 10">
            <ComboBoxItem>One</ComboBoxItem>
            <ComboBoxItem>Two</ComboBoxItem>
            <ComboBoxItem>Three</ComboBoxItem>
        </ComboBox>
        <TextBox Text="Row 11" />
        <TextBox Text="Row 12" />
        <TextBox Text="Row 13" />
        <TextBox Text="Row 14" />
        <TextBox Text="Row 15" />
        <TextBox Text="Row 16" />
        <TextBox Text="Row 17" />
        <TextBox Text="Row 18" />
        <TextBox Text="Row 19" />
        <TextBox Text="Row 20" />
    </StackPanel>
</ScrollViewer>
</Window>
0

There are 0 best solutions below