Search code examples
wpfvb.netdata-bindingribbon

RibbonGallery.SelectedValue not updating SelectedItem


I'm using the Microsoft ribbon library: System.Windows.Controls.Ribbon. I know this question looks big, but what I'm trying to do is actually not that complicated, there are just a few pieces involved.

The Goal

I'm trying to bind the selection of a RibbonComboBox to a property of one of my classes, which I'll call TestBindingSource, but I need to be able to cancel the change of selection. So if they select an item from the RibbonComboBox, but then cancel that change, the selection needs to stay as it was.

The items being show in the RibbonComboBox represent members of an Enum, which I'll call TestEnum. I built another class TestEnumGalleryItem to represent the TestEnum values in the RibbonComboBox and I use the RibbonGallery.SelectedValue and RibbonGallery.SelectedValuePath to bind to the property on TestBindingSource. In case that was too hard to follow, you should be able to see what I mean from my code.

The Code

The below is a simplified version of my real code, try not to dock me too many points for style. I've tested this in a new project and it can be used to show the problem I'm running into. Remember to add a reference to the Microsoft ribbon library.

MainWindow.xaml

<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:VBTest"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Ribbon>
            <RibbonTab Header="Test">
                <RibbonGroup>
                    <RibbonComboBox Name="TestComboBox">
                        <RibbonGallery Name="TestGallery" MaxColumnCount="1" ScrollViewer.VerticalScrollBarVisibility="Auto" SelectedValuePath="EnumValue" SelectedValue="{Binding BindingSource.TestEnumValue}">
                            <RibbonGallery.ItemsSource>
                                <x:Array Type="local:TestEnumGalleryCategory">
                                    <local:TestEnumGalleryCategory/>
                                </x:Array>
                            </RibbonGallery.ItemsSource>
                            <RibbonGallery.CategoryTemplate>
                                <HierarchicalDataTemplate ItemsSource="{Binding Items}">
                                    <HierarchicalDataTemplate.ItemTemplate>
                                        <DataTemplate>
                                            <RibbonGalleryItem ToolTipTitle="{Binding EnumName}" ToolTipDescription="{Binding EnumDescription}">
                                                <TextBlock Text="{Binding EnumName}" Margin="0, -3, -0, -3"/>
                                            </RibbonGalleryItem>
                                        </DataTemplate>
                                    </HierarchicalDataTemplate.ItemTemplate>
                                </HierarchicalDataTemplate>
                            </RibbonGallery.CategoryTemplate>
                        </RibbonGallery>
                    </RibbonComboBox>

                    <RibbonButton Label="Break" Click="RibbonButton_Click"/>
                </RibbonGroup>                
            </RibbonTab>
        </Ribbon>
    </Grid>
</Window>

MainWindow.xaml.vb

Imports System.ComponentModel

Class MainWindow
    Public Sub New()
        BindingSource = New TestBindingSource
        InitializeComponent()
    End Sub

    Public Property BindingSource As TestBindingSource
        Get
            Return GetValue(BindingSourceProperty)
        End Get
        Set(ByVal value As TestBindingSource)
            SetValue(BindingSourceProperty, value)
        End Set
    End Property
    Public Shared ReadOnly BindingSourceProperty As DependencyProperty =
                           DependencyProperty.Register("BindingSource",
                           GetType(TestBindingSource), GetType(MainWindow))

    Private Sub RibbonButton_Click(sender As Object, e As RoutedEventArgs)
        Stop
    End Sub
End Class

Public Enum TestEnum
    ValueA
    ValueB
End Enum

Public Class TestEnumGalleryCategory
    Public Property Items As New List(Of TestEnumGalleryItem) From {New TestEnumGalleryItem With {.EnumValue = TestEnum.ValueA, .EnumName = "Value A", .EnumDescription = "A's description"},
                                                                    New TestEnumGalleryItem With {.EnumValue = TestEnum.ValueB, .EnumName = "Value B", .EnumDescription = "B's description"}}
End Class

Public Class TestEnumGalleryItem
    Public Property EnumValue As TestEnum = TestEnum.ValueA

    Public Property EnumName As String

    Public Property EnumDescription As String
End Class

Public Class TestBindingSource
    Implements INotifyPropertyChanged

    Private _TestEnumValue As TestEnum = TestEnum.ValueA
    Property TestEnumValue As TestEnum
        Get
            Return _TestEnumValue
        End Get
        Set(value As TestEnum)
            'Don't actually set new value, just leave it the same to simulate cancelation
            RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(NameOf(TestEnumValue)))
        End Set
    End Property

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
End Class

The Problem

You'll see when you run the code that the RibbonComboBox displays "Value A" by default. Change the selection to "Value B". The selection of the RibbonComboBox changes and now displays "Value B". This is not what I want to happen, the selection should immediately change back to "Value A".

If you look at the code for TestBindingSource.TestEnumValue, you'll see that I don't actually keep the new value when set, instead I leave the old one to simulate the user canceling the change. I then raise the PropertyChanged event to update the UI so it knows what the real value of the property is.

After you've changed to "Value B", click the "Break" button (included for convenience). In the Visual Studio watch window, compare the values of TestGallery.SelectedItem and TestGallery.SelectedValue. You'll see that TestGallery.SelectedValue holds the correct TestEnum value ValueA. Now look at TestGallery.SelectedItem and you'll see that it still holds the item representing ValueB.

So even though the RibbonGallery has been properly informed that the value should now be ValueA, it's still showing ValueB. How can I fix that?

I'll level with you, I don't have a ton of time to spend on this bug, and I'm used to having to make hacky workarounds when it comes to the ribbon. Any solution you can give me on how to get the RibbonGallery (and hence the RibbonComboBox) to update correctly will be appreciated.


Solution

  • After more testing and research, I realized that this problem is not unique to the ribbon library. It actually seems to be a problem with the normal ComboBox too and presumably all ItemsControls. Once I realized that, I was able to search for an answer more effectively and found the solution here:
    https://nathan.alner.net/2010/04/25/cancelling-selection-change-in-a-bound-wpf-combo-box/

    It's not a perfectly clean solution, but in my specific case setting the value to the new selection and then immediately setting it back doesn't cause any problems, so that's what I did. For the record, when setting the selection back, I used DispatcherPriority.DataBind instead of DispatcherPriority.ContextIdle, this way the change never even shows in the UI but the solution still works.