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.
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 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
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.
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.