RibbonGallery.SelectedValue not updating SelectedItem - wpf

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.

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.

Related

Mark as read only all controls WPF

OK this is an interesting one.
I have a wpf application with tabs. What I want to do is have a DB setting that turns off the ability to edit all textboxs. What I was thinking was to bring in the value, if the value is true then I would turn all the text boxes to read only.
I have seen this example:
private void DisableControls(Control con)
{
foreach (Control c in controls)
{
DisableControls(c);
}
con.Enabled = false;
}
However I get red squiggly line under controls and again under Enabled. I will preface this by saying I am new to WPF.
Does anyone have a solution to this (or even a better way) any pointing in the right way would help.
Create a view model that wraps your database models
public class MyViewModel : INotifyPropertyChanged
{
public bool MakeReadOnly {get;set;}
}
Reference your view model in the View
<Window x:Class="Example.MainWindow"
...
xmlns:local="clr-namespace:Example"
...>
<Window.Resources>
<local:MyViewModel x:Key="ViewModel"/>
</Window.Resources>
...
</Window>
Bind the boolean value to your textboxes IsReadOnly property
<TextBox x:Name="FirstName" IsReadOnly="{Binding MakeReadOnly">
The user may not modify the contents of this TextBox if marked as readonly
</TextBox>
<TextBox x:Name="LastName" IsReadOnly="{Binding MakeReadOnly">
The user may not modify the contents of this TextBox if marked as readonly
</TextBox>
More on View Models here
Hope this helps!

ItemsControl DataTemplate when using an arbitrary UserControl collection

Context
I am working on a all-in-one virtual desktop for my users where most tasks require input through a modal dialog. Some super users can navigate through many kinds of configuration dialogs, which leads to serveral occurences of 2nd level dialogs (the first modal dialog invokes another one). In compliance with internal design orientations, each of those dialogs are placed on top of a semi-transparent grayed out background.
Trying to avoid writing the gray-background-panel over and over, I figured I could use an ItemsControl in order to stack my dialogs. My proof of concept worked wonders using a collection of String. So far so good.
The problem
Things get odd when using a collection of UserControl as the ItemsSource. WPF displays the actual UserControl instead of the ItemTemplate. It feels like the template is not even used when the collection's items are UIElements.
Xaml
<ItemsControl ItemsSource="{Binding SomeList}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Background="#AA000000">
<Label Content="Does it work" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Code Behind
Public ReadOnly Property SomeOtherList As UserControl()
Get
Return New UserControl() {New MyControl}
End Get
End Property
Public ReadOnly Property SomeList As String()
Get
Return New String() {"One item"}
End Get
End Property
The actual question
Is there any way to specify a template when the ItemsSource items already possess one? Going further, can we even template a wrapper arround UserControl while leaving the actual UserControl untouched?
I know the whole thing could be cheated using code behind, but relying on VB or C# would not go through code review unnoticed. We're looking for an XAML solution here.
P.S. I am open to new solutions as long as there is a single, unified way to invoke an arbitrary number of dialogs.
After two days of tests I admited the initial approach was not possible, or at least to the extend of my knowledge. I came up with a decent wrapping solution.
Wrapper
<UserControl x:Class="HolderPopup"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid Background="#AA000000" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Border Style="{StaticResource ResourceKey=borderBase}" />
<Grid Name="placeHolder" Margin="10" />
</Grid>
</UserControl>
The wrapper's code behind has a constructor to which you pass a UserControl to be placed in placeHolder's children, which lets you use it as shown below:
Usage
Private Shared _popup As ObservableCollection(Of UserControl)
Private Shared ReadOnly Property Popup As ObservableCollection(Of UserControl)
Get
If _popup Is Nothing Then _popup = New ObservableCollection(Of UserControl)
Return _popup
End Get
End Property
Public Shared Sub ModalPush(item As UserControl)
Popup.Add(New HolderPopup(item))
End Sub
Public Shared Sub ModalPop()
If Popup.Count > 0 Then Popup.RemoveAt(Popup.Count - 1)
End Sub
'For WPF binding
Public ReadOnly Property PopupRef As ObservableCollection(Of UserControl)
Get
Return Main.Popup()
End Get
End Property
Any event handler, anywhere in the application can call Main.ModalPush in order to stack a modal window on top of whathever was already there.
While this solution respects the constraints (unified popup handling without forcing some hackish dependency in my popups) I am not entirely satisfied. I feel it should have been possible through templating, which would have the benefit of removing this new wrapper class. All in all, that's an alternative, but not exactly what I was looking for.

Trying to bind a list of objects to a WPF datagrid control and seeing blank lines

I am stuck and need some help. I have spent a full day trying to sort this one out. I have read a lot of examples of how this should be done and it appears that I have done it correctly but clearly I have missed something. I have a blank form with one button and one datagrid. When I click the button I want to load a list of points into the datagrid control. The data must be getting loaded because I see three blank lines in the grid (no headers), but no data. Help please! Thanks!
Class MainWindow
Class Point
Public Inc As String
Public AZ As String
Public MD As String
Public TD As String
End Class
Private Sub Button_Click(sender As Object, e As RoutedEventArgs)
Dim mySurvey As New List(Of Point)
Dim myPoint1 As New Point
Dim myPoint2 As New Point
myPoint1.AZ = "1"
myPoint1.Inc = "2"
myPoint1.MD = "100"
myPoint1.TD = "98"
myPoint2.AZ = "10"
myPoint2.Inc = "20"
myPoint2.MD = "1000"
Point2.TD = "980"
mySurvey.Add(myPoint1)
mySurvey.Add(myPoint2)
dgSurvey.ItemsSource = mySurvey
End Sub
End Class
XAML
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Button Content="Button" HorizontalAlignment="Left" Height="28" Margin="45,30,0,0" VerticalAlignment="Top" Width="57" Click="Button_Click"/>
<DataGrid x:Name="dgSurvey" HorizontalAlignment="Left" Margin="45,78,0,0" VerticalAlignment="Top" Height="172" Width="275"/>
</Grid>
For binding to work you should define properties in your Point class, not public fields.
This is explicitly stated in MSDN: Binding Sources Overview:
The properties you use as binding source properties for a binding must be public
properties of your class. Explicitly defined interface properties cannot be
accessed for binding purposes, nor can protected, private, internal,
or virtual properties that have no base implementation.
You cannot bind to public fields.

Setting content of TextBlock and text of HyperlinkButton in silverlight custom control

I am trying to create a custom control that will display a hyperlink button with some text below the link. The idea is to have urgent messages show up on a screen of a Silverlight page. From what I have read, I thought that I should be able to create a new control and then create some dependancy properties and bind the dynamic parts of the component pieces to them in order to allow me to add multiple instances of the custom control to my Silverlight project. Here is my XAML that defines the control
<UserControl
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"
mc:Ignorable="d"
x:Class="WhatsNew.UrgentStoryGridControl"
d:DesignWidth="608" d:DesignHeight="65" Background="White">
<UserControl.Resources>
<Style x:Key="WhatsNewTitleStyle" TargetType="HyperlinkButton">
Removed for Brevity
</Style>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Height="65" Margin="0" VerticalAlignment="Bottom" Background="White">
<StackPanel>
<HyperlinkButton Style="{StaticResource WhatsNewTitleStyle}" Content="{Binding linkText}" HorizontalAlignment="Left" VerticalAlignment="Top" NavigateUri="{Binding linkURI}" Foreground="Red"/>
<TextBlock Style="{StaticResource WhatsNewTextStyle}" Text="{Binding storyText}" Margin="0,13,0,0" d:LayoutOverrides="Height"/>
</StackPanel>
</Grid>
In the code behind, I have created three dependancy properties
Partial Public Class UrgentStoryGridControl
Inherits UserControl
Public Shared linkTextProperty As DependencyProperty = DependencyProperty.Register("linkText", GetType(String), GetType(UrgentStoryGridControl), New PropertyMetadata("Link Text"))
Public Shared linkURIProperty As DependencyProperty = DependencyProperty.Register("linkURI", GetType(String), GetType(UrgentStoryGridControl), New PropertyMetadata("link.html"))
Public Shared storyTextProperty As DependencyProperty = DependencyProperty.Register("storyText", GetType(String), GetType(UrgentStoryGridControl), New PropertyMetadata("Story Text"))
Public Property linkText() As String
Get
Return GetValue(linkTextProperty)
End Get
Set(ByVal value As String)
SetValue(linkTextProperty, value)
End Set
End Property
Public Property linkURI() As String
Get
Return GetValue(linkURIProperty)
End Get
Set(ByVal value As String)
SetValue(linkURIProperty, value)
End Set
End Property
Public Property storyText As String
Get
Return GetValue(storyTextProperty)
End Get
Set(ByVal value As String)
SetValue(storyTextProperty, value)
End Set
End Property
End Class
When I place this control on my Silverlight project using Expression Blend, I see the three properties listed in the Miscellaneous section of the properties window as I would expect. The values from the PropertyMetadata are populated as the default values for these properties. Here is the code from my Silverlight project where I leave the default values alone:
<local:UrgentStoryGridControl x:Name="urgentStory" Height="65" />
Here is the code where I try to set the values to something:
<local:UrgentStoryGridControl x:Name="urgentStory" Height="65" linkText="Test Link Text" linkURI="testpage.html" storyText="Sample Story Text" />
Either way I attempt to use the control, I'm not getting anything displayed when I launch the application. I figure that I'm missing something small but after having spent a lot of time today researching this, I'm not finding anything that would indicate what I'm missing or doing wrong.
You need to set the DataContext in your custom UserControl or else your bindings won't work.
In your UrgentStoryGridControl's constructor, you should be able to set Me.DataContext = Me

WPF DataGrid and Avalon TimePicker binding is not working

I'm using a the WPF DataGrid from the wpf toolkit and a TimePicker from AvalonControlsLibrary to insert a collection of TimeSpans. My problem is that bindings are not working inside the DataGrid, and I have no clue of why this isn't working.
Here is my setup:
I have the following XAML:
<Window x:Class="TestMainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:wpf="http://schemas.microsoft.com/wpf/2008/toolkit" xmlns:a="http://schemas.AvalonControls/AvalonControlsLibrary/Controls" SizeToContent="WidthAndHeight" MinHeight="250" MinWidth="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<GroupBox Grid.Row="0">
<GroupBox.Header>
Testing it:
</GroupBox.Header>
<wpf:DataGrid ItemsSource="{Binding Path=TestSpans}" AutoGenerateColumns="False">
<wpf:DataGrid.Columns>
<wpf:DataGridTemplateColumn Header="Start">
<wpf:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<a:TimePicker SelectedTime="{Binding Path=Span, Mode=TwoWay}" />
</DataTemplate>
</wpf:DataGridTemplateColumn.CellEditingTemplate>
<wpf:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Span}" />
</DataTemplate>
</wpf:DataGridTemplateColumn.CellTemplate>
</wpf:DataGridTemplateColumn>
</wpf:DataGrid.Columns>
</wpf:DataGrid>
</GroupBox>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row="1">
<a:TimePicker SelectedTime="{Binding Path=SelectedTime, Mode=TwoWay}" />
</StackPanel>
</Grid>
And this is my ViewModel:
Imports System.Collections.ObjectModel
Public Class TestMainWindowViewModel
Private _selectedTime As TimeSpan = DateTime.Now.TimeOfDay
Public Property SelectedTime() As TimeSpan
Get
Return _selectedTime
End Get
Set(ByVal value As TimeSpan)
_selectedTime = value
End Set
End Property
Private _testSpans As ObservableCollection(Of TimeSpanContainer) = New ObservableCollection(Of TimeSpanContainer)
Public Property TestSpans() As ObservableCollection(Of TimeSpanContainer)
Get
Return _testSpans
End Get
Set(ByVal value As ObservableCollection(Of TimeSpanContainer))
_testSpans = value
End Set
End Property
Public Sub New()
_testSpans.Add(DateTime.Now.TimeOfDay)
_testSpans.Add(DateTime.Now.TimeOfDay)
_testSpans.Add(DateTime.Now.TimeOfDay)
End Sub
End Class
Public Class TimeSpanContainer
Private _span As TimeSpan
Public Property Span() As TimeSpan
Get
Return _span
End Get
Set(ByVal value As TimeSpan)
_span = value
End Set
End Property
Public Sub New(ByVal t As TimeSpan)
_span = t
End Sub
End Class
I'm starting this window in application.xaml.vb like this:
Class Application
' Application-level events, such as Startup, Exit, and DispatcherUnhandledException
' can be handled in this file.
Protected Overrides Sub OnStartup(ByVal e As System.Windows.StartupEventArgs)
MyBase.OnStartup(e)
Dim window As TestMainWindow = New TestMainWindow
window.DataContext = New TestMainWindowViewModel()
window.Show()
End Sub
End Class
EDIT 1: I forgot to mention that the binding to SelectedTime TimeSpan works as expected. The problem are the bindings inside the DataGrid.
EDIT 2: Changed example a little bit to show the problem better.
What do you mean by your bindings aren't working? Are you getting no value in the timepicker control when you attempt to edit the value?
Edit:
Ok, I was having this same issue yesterday and I think there is 2 parts to the issue.
If there is no value appearing in the TimePicker control when you switch to edit mode then there is probably a binding issue with the control.
The binding to the underlying value I found to be an issue with using the DataGridTemplateColumn. Basically the grid doesnt handle your databinding back using the same mechanisms of regular bound columns. What it means is you need to perform the following binding on your controls within the column:
SelectedTime="{Binding Span, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
This will fix the binding back to the underlying object. However if there is still an issue with the control it may not help you much. Sorry but I haven't used AvalonControlsLibrary so not sure if there is a potential problem there. Fixing step 2 solved my issues.
Cheers
-Leigh
I know this is an old question, but I where playing with this exact control having the exact same issue. I looked in the TimePicker class of AvalonControlsLibrary and the constructor looks like this
/// <summary>
/// Default constructor
/// </summary>
public TimePicker()
{
SelectedTime = DateTime.Now.TimeOfDay;
}
Removing the line setting the SelectedTime restored the databinding behaviour for me making your posted example work as intended.

Resources