ComboBox.SelectedValue not updating from binding source - wpf

Here's my binding source object:
Public Class MyListObject
Private _mylist As New ObservableCollection(Of String)
Private _selectedName As String
Public Sub New(ByVal nameList As List(Of String), ByVal defaultName As String)
For Each name In nameList
_mylist.Add(name)
Next
_selectedName = defaultName
End Sub
Public ReadOnly Property MyList() As ObservableCollection(Of String)
Get
Return _mylist
End Get
End Property
Public ReadOnly Property SelectedName() As String
Get
Return _selectedName
End Get
End Property
End Class
Here is my XAML:
<Window x:Class="Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300"
xmlns:local="clr-namespace:WpfApplication1"
>
<Window.Resources>
<ObjectDataProvider x:Key="MyListObject" ObjectInstance="" />
</Window.Resources>
<Grid>
<ComboBox Height="23"
Margin="24,91,53,0"
Name="ComboBox1"
VerticalAlignment="Top"
SelectedValue="{Binding Path=SelectedName, Source={StaticResource MyListObject}, Mode=OneWay}"
ItemsSource="{Binding Path=MyList, Source={StaticResource MyListObject}, Mode=OneWay}"
/>
<Button Height="23"
HorizontalAlignment="Left"
Margin="47,0,0,87"
Name="btn_List1"
VerticalAlignment="Bottom"
Width="75">List 1</Button>
<Button Height="23"
Margin="0,0,75,87"
Name="btn_List2"
VerticalAlignment="Bottom"
HorizontalAlignment="Right"
Width="75">List 2</Button>
</Grid>
</Window>
Here's the code-behind:
Class Window1
Private obj1 As MyListObject
Private obj2 As MyListObject
Private odp As ObjectDataProvider
Public Sub New()
InitializeComponent()
Dim namelist1 As New List(Of String)
namelist1.Add("Joe")
namelist1.Add("Steve")
obj1 = New MyListObject(namelist1, "Steve")
.
Dim namelist2 As New List(Of String)
namelist2.Add("Bob")
namelist2.Add("Tim")
obj2 = New MyListObject(namelist2, "Tim")
odp = DirectCast(Me.FindResource("MyListObject"), ObjectDataProvider)
odp.ObjectInstance = obj1
End Sub
Private Sub btn_List1_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles btn_List1.Click
odp.ObjectInstance = obj1
End Sub
Private Sub btn_List2_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles btn_List2.Click
odp.ObjectInstance = obj2
End Sub
End Class
When the Window first loads, the bindings hook up fine. The ComboBox contains the names "Joe" and "Steve" and "Steve" is selected by default. However, when I click a button to switch the ObjectInstance to obj2, the ComboBox ItemsSource gets populated correctly in the dropdown, but the SelectedValue is set to Nothing instead of being equal to obj2.SelectedName.

We had a similar issue last week. It has to do with how SelectedValue updates its internals. What we found was if you set SelectedValue it would not see the change we had to instead set SelectedItem which would properly update every thing. My conclusion is that SelectedValue is designed for get operations and not set. But this may just be a bug in the current version of 3.5sp1 .net

To stir up a 2 year old conversation:
Another possibility, if you're wanting to use strings, is to bind it to the Text property of the combobox.
<ComboBox Text="{Binding Test}">
<ComboBoxItem Content="A" />
<ComboBoxItem Content="B" />
<ComboBoxItem Content="C" />
</ComboBox>
That's bound to something like:
public class TestCode
{
private string _test;
public string Test
{
get { return _test; }
set
{
_test = value;
NotifyPropertyChanged(() => Test); // NotifyPropertyChanged("Test"); if not using Caliburn
}
}
}
The above code is Two-Way so if you set Test="B"; in code then the combobox will show 'B', and then if you select 'A' from the drop down then the bound property will reflect the change.

Use
UpdateSourceTrigger=PropertyChanged
in the binding

The type of the SelectedValuePath and the SelectedValue must be EXACTLY the same.
If for example the type of SelectedValuePath is Int16 and the type of the property that binds to SelectedValue is int it will not work.
I spend hours to find that, and that's why I am answering here after so much time the question was asked. Maybe another poor guy like me with the same problem can see it.

Problem:
The ComboBox class searches for the specified object by using the IndexOf method. This method uses the Equals method to determine equality.
Solution:
So, try to set SelectedIndex using SelectedValue via Converter like this:
C# code
//Converter
public class SelectedToIndexConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value != null && value is YourType)
{
YourType YourSelectedValue = (YourType) value;
YourSelectedValue = (YourType) cmbDowntimeDictionary.Tag;
YourType a = (from dd in Helper.YourType
where dd.YourTypePrimaryKey == YourSelectedValue.YourTypePrimaryKey
select dd).First();
int index = YourTypeCollection.IndexOf(a); //YourTypeCollection - Same as ItemsSource of ComboBox
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value!=null && value is int)
{
return YourTypeCollection[(int) value];
}
return null;
}
}
Xaml
<ComboBox
ItemsSource="{Binding Source={StaticResource YourDataProvider}}"
SelectedIndex="{Binding Path=YourValue, Mode=TwoWay, Converter={StaticResource SelectedToIndexConverter}, UpdateSourceTrigger=PropertyChanged}"/>
Good luck! :)

Ran into something similar, finally I just subscribed to the SelectionChanged event for the drop down and set my data property with it. Silly and wish it was not needed, but it worked.

Is it reasonable to set the SelectedValuePath="Content" in the combobox's xaml, and then use SelectedValue as the binding?
It appears that you have a list of strings and want the binding to just do string matching against the actual item content in the combobox, so if you tell it which property to use for the SelectedValue it should work; at least, that worked for me when I ran across this problem.
It seems like Content would be a sensible default for SelectedValue but perhaps it isn't?

Have you tried raising an event that signals SelectName has been updated, e.g., OnPropertyChanged("SelectedName")? That worked for me.

In my case I was binding to a list while I should be binding to a string.
What I was doing:
private ObservableCollection<string> _SelectedPartyType;
public ObservableCollection<string> SelectedPartyType { get { return
_SelectedPartyType; } set {
_SelectedPartyType = value; OnPropertyChanged("SelectedPartyType"); } }
What should be
private string _SelectedPartyType;
public string SelectedPartyType { get { return _SelectedPartyType; } set {
_SelectedPartyType = value; OnPropertyChanged("SelectedPartyType"); } }

The Binding Mode needs to be OneWayToSource or TwoWay since the source is what you want updated. Mode OneWay is Source to Target and therefore makes the Source ReadOnly which results in never updating the Source.

You know... I've been fighting with this issue for hours today, and you know what I found out? It was a DataType issue! The list that was populating the ComboBox was Int64, and I was trying to store the value in an Int32 field! No errors were being thrown, it just wasn't storing the values!

Just resolved this. Huh!!!
Either use [one of...] .SelectedValue | .SelectedItem | .SelectedText
Tip: Selected Value is preferred for ComboStyle.DropDownList while .SelectedText is for ComboStyle.DropDown.
-This should solve your problem. took me more than a week to resolve this small fyn. hah!!

Related

Enum getting cast to string when bound with custom converter

Problem Overview
I have a custom IValueConverter called EnumDisplayConverter. It's supposed to take an Enum value and return the name so it can be displayed. Somehow, even though this converter is being used on a binding between properties of an Enum type, the converter is being passed a value of String.Empty. This of course causes an error as String is not an Enum, not to mention it's just really unexpected.
Code to Reproduce
The following code can be used to reproduce the error. The steps to reproduce and an explanation of what the code is meant to do come after.
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:VBTest"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<DockPanel>
<ListBox Name="LB_Foos" DockPanel.Dock="Left" ItemsSource="{Binding FooOptions}" SelectionChanged="ListBox_SelectionChanged"/>
<ComboBox ItemsSource="{x:Static local:MainWindow.SelectableThings}" SelectedItem="{Binding OpenFoo.SelectableChosenThing}" VerticalAlignment="Center" HorizontalAlignment="Center" Width="100">
<ComboBox.ItemTemplate>
<DataTemplate>
<ContentControl>
<ContentControl.Style>
<Style TargetType="ContentControl">
<Setter Property="Content" Value="{Binding Converter={x:Static local:EnumDisplayConverter.Instance}}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding}" Value="-1">
<Setter Property="Content" Value="None"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</DockPanel>
</Window>
Imports System.Collections.ObjectModel
Imports System.Globalization
Class MainWindow
Shared Sub New()
Dim Things = (From v As Thing In [Enum].GetValues(GetType(Thing))).ToList
Things.Insert(0, -1)
SelectableThings = New ReadOnlyCollection(Of Thing)(Things)
End Sub
Public Shared ReadOnly Property SelectableThings As IReadOnlyList(Of Thing)
Public ReadOnly Property FooOptions As New ReadOnlyCollection(Of Integer)({1, 2, 3, 4})
'This is a placeholder method meant to set OpenFoo to a new instance of Foo when a selection is made.
'In the actual application, this is done with data binding and involves async database calls.
Private Sub ListBox_SelectionChanged(sender As Object, e As SelectionChangedEventArgs)
OpenFoo = Nothing
Select Case LB_Foos.SelectedItem
Case 1
OpenFoo = New Foo With {.ChosenThing = Nothing}
Case 2
OpenFoo = New Foo With {.ChosenThing = Thing.A}
Case 3
OpenFoo = New Foo With {.ChosenThing = Thing.B}
Case 4
OpenFoo = New Foo With {.ChosenThing = Thing.C}
End Select
End Sub
Public Property OpenFoo As Foo
Get
Return GetValue(OpenFooProperty)
End Get
Set(ByVal value As Foo)
SetValue(OpenFooProperty, value)
End Set
End Property
Public Shared ReadOnly OpenFooProperty As DependencyProperty =
DependencyProperty.Register("OpenFoo",
GetType(Foo), GetType(MainWindow))
End Class
Public Enum Thing
A
B
C
End Enum
Public Class Foo
Public Property ChosenThing As Thing?
Public Property SelectableChosenThing As Thing
Get
Return If(_ChosenThing, -1)
End Get
Set(value As Thing)
Dim v As Thing? = If(value = -1, New Thing?, value)
ChosenThing = v
End Set
End Property
End Class
Public Class EnumDisplayConverter
Implements IValueConverter
Public Shared ReadOnly Property Instance As New EnumDisplayConverter
Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert
If value Is Nothing Then Return Nothing
Return [Enum].GetName(value.GetType, value)
End Function
Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
Return Binding.DoNothing
End Function
End Class
Steps to Reproduce
Run MainWindow
Select any item from the ListBox on the left
Select a different item from the ListBox
Observe unhanded exception
Code Explanation
In case it's not clear what the code is supposed to do, I'll explain a bit.
Foo represents a data object that is being edited by the user via MainWindow. Every Foo has the option of a Thing. Not having a Thing is also an option, which is why ChosenThing is a Thing? (i.e. Nullable(Of Thing)).
Null data items don't work in a ComboBox, since Null means "there is no selection". To get around this, I add a value of -1 to my list of selectable Thing values. In Foo.SelectableChosenThing, I check for -1 and convert it to Null for the actual value of Foo.ChosenThing. This lets me bind to the ComboBox correctly.
Problem Details
The error only seems to occur when OpenFoo is set to Nothing before being given a new value. If I take out the line OpenFoo = Nothing, everything works. However, in the real application I want to set OpenFoo to Nothing while the selection is being loaded- besides, it doesn't explain why this is happening in the first place.
Why is EnumDisplayConverter being passed a value of type String, when the properties involved are of the expected type Thing?
Turns out, after some investigation, that the problem comes from the ComboBox control. When the selected item of the ComboBox is null, it replaces the display value of null with String.Empty. Here's a snippet from the .NET Reference Source for ComboBox.UpdateSelectionBoxItem:
...
// display a null item by an empty string
if (item == null)
{
item = String.Empty;
itemTemplate = ContentPresenter.StringContentTemplate;
}
SelectionBoxItem = item;
SelectionBoxItemTemplate = itemTemplate;
...
This happens before any DataTemplates are considered, so by the time my DataTemplate get's called, it's being given a value of String.Empty to display instead of null.
The solution is to either
Add a DataTrigger to the ContentControl's Style which checks for String.Empty and doesn't use the converter in such a case.
Modify EnumDisplayConverter to check for non-Enum values and return DependencyProperty.UnsetValue.

databinding datagrid's comboboxes and textboxes to staticresource list

I've created a WPF vb.net form containing datagrid. Inside of datagrid I have two comboboxes and two textboxes, one combobox textbox pair for articles and another one for services. I should bind first combo to property of List(Of article) type and second to List(Of service) type where article is public class containing two public properties (articleId and articleName) and service is public class containing two public properties (serviseId and serviceName). Textboxes should display article and service names and comboboxes should display IDs. When combo selection is changed textbox text should change its value too.
List(Of article) and List(Of service) should be populated from database.
How could I do this, I know that solution is somewhere around me but can't catch it at all. There are two main problems, binding controls and populatin list from database.
If I need to post a part of code I'm gonna do this, just let me know.
Please help me to solve this situation,
thanks.
First of all - sorry, I am not very familiar with VB.Net, so my code is written in c#. Since I have no understanding of what you actually need, I can only offer you a following not really elegant solution.
For binding your ComboBox to the items from database you need to use a StaticResource, as I can see you did it, but maybe you defined it wrong, that's why it is not working for you. To get it work you need to have a class like this:
public class artiklsList : List<artikl>
{
public artiklsList()
{
this.Add(new artikl(1, "first")); //this is dummy items, you need to do a database stuff here
this.Add(new artikl(2, "second"));
this.Add(new artikl(3, "third"));
}
}
And a xaml like this:
<Window.Resources>
<my:artiklsList x:Key="source"></my:artiklsList>
</Window.Resources>
You also need update a cell with text when a ComboBox selection changed. It's not a simple task, because the easiest way to do this using Binding to the control using ElementName will not work in the DataGrid. What I did is kinda hacky anyway...
So, xaml is not very complicated:
<DataGrid Name="dgrStavke" AutoGenerateColumns="False" Height="160" Width="600" HorizontalAlignment="Left" Margin="5" Grid.Row="7" Grid.ColumnSpan="4" >
<DataGrid.Columns>
<DataGridTemplateColumn Header="Artikl ID">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox SelectedIndex="{Binding selectedIndexID, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Name="cmbArtikli" Width="120" DisplayMemberPath="artiklId" ItemsSource="{StaticResource source}">
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Naziv artikla" Binding="{Binding nazivArtikla}"/>
</DataGrid.Columns>
</DataGrid>
The binding of SelectedIndex and a horrible code-behind doing the trick with updating a text cell. For binding to work properly articl class have to implement INotifyPropertyChanged interface.
public class artikl: INotifyPropertyChanged
{
public artikl(int artid, string nazivart)
{
artiklId = artid;
nazivArtikla = nazivart;
}
public int artiklId{get;set;}
private string _nazv;
public string nazivArtikla
{
get { return _nazv; }
set { _nazv = value; NotifyPropertyChanged("nazivArtikla"); }
}
//Here I think you may have questions
private int _index;
public int selectedIndexID
{
get
{
//To get a SelectedIndex for ComboBox in current row we look in
//listArtikli defined in a MainWindow for a articli item with a current
//item's Id and take the index of this item
artikl art = MainWindow.listArtikli.Find(el => el.artiklId == this.artiklId);
return MainWindow.listArtikli.IndexOf(art);
}
set
{
//This property is binded with SelectedIndex property of ComboBox.
//When selected index changed, we look in listArtikli and take
//here properties of item with this index.
//This will change values of item binded to the current grid row
_index = value;
this.nazivArtikla = MainWindow.listArtikli[value].nazivArtikla;
this.artiklId = MainWindow.listArtikli[value].artiklId;
NotifyPropertyChanged("selectedIndexID");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And a window code-behind:
public partial class MainWindow : Window
{
public static artiklsList listArtikli = new artiklsList();
public static artiklsList gridsource = new artiklsList();
public MainWindow()
{
InitializeComponent();
dgrStavke.ItemsSource = gridsource;
}
}
You need gridsource aside from listArtikli to fill your DataGrid with values. Also because of all this code in selectedIndexID if you use only listArtikli its values corrupting. So listArtikli contains articli items just sorted in order which they were retrieved. And gridsource contains pairs artiklId-nazivArtikla as they presented in the DataGrid.
Hope it will help you just a little.
thanks for interesting. If you need more clarification please let me know. This is the code for one pair combobox-textbox, I'll easily apply this for another one:
XAML
...
<DataGrid Name="dgrStavke" AutoGenerateColumns="False" Height="160" Width="600" HorizontalAlignment="Left" Margin="5" Grid.Row="7" Grid.ColumnSpan="4">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Artikl ID">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox Name="cmbArtikli" Width="120" ItemsSource="{Binding Source={StaticResource artcls}, Path=listArtikli}" DisplayMemberPath="artiklId"></ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Naziv artikla" Binding="{Binding nazivArtikla}"></DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
...
CODE
Imports System.Data
Imports System.Data.SqlClient
Imports System.Text
Namespace MW
Public Class artikl
Sub New(artid As Integer, nazivart As String)
' TODO: Complete member initialization
artiklId = artid
nazivArtikla = nazivart
End Sub
Public Property artiklId() As Integer
Public Property nazivArtikla() As String
End Class
Public Class frmDodavanjePaketa
Public Property listArtikli() As New List(Of artikl)
Private Sub popuniComboArtikli()
Dim sqlConn As SqlConnection = New SqlConnection(moduleGlobal.connString)
sqlConn.Open()
Dim strSql As New StringBuilder
strSql.Append("select a.artiklId, a.nazivArtikla ")
strSql.Append(" from artikli a ")
strSql.Append(" where isnull(a.aktivan, 0) = 1")
Dim sqlCom As SqlCommand = New SqlCommand(strSql.ToString, sqlConn)
Dim sqlDs As DataSet = New DataSet
Dim sqlDa As SqlDataAdapter = New SqlDataAdapter
sqlDa.SelectCommand = sqlCom
sqlDa.Fill(sqlDs)
For Each row As DataRow In sqlDs.Tables(0).Rows
Me.listArtikli.Add(New artikl(row.ItemArray(0).ToString, row.ItemArray(1).ToString))
Next
End Sub
End Class
End Namespace

Adding Items to a DataGrid (MVVM)

Goal
To add a list of a custom class object (DamagedItems) to a DataGrid using the Model, View, ViewModel (MVVM) way of doing things.
I want the user to be able to create entries of damaged parts (deemed improper during inspection of a machine).
What I have done
I have created:
A window: wDamagedItems.xaml in which it's DataContext is set to DamagedItemViewModel
A Model: DamagedItemModel.vb which implements INotifyPropertyChanged
A ViewModel: DamagedItemViewModel.vb where I set properties of classes such as my DamagedItemModel
An ObservableCollection: DamagedItemList.vb which inherits an ObservableCollection(Of DamagedItemModel)
Since my DataContext is set to the DamagedItemViewModel, here is how I setup the properties:
Public Class DamagedItemViewModel
Private _DamagedItem As DamagedItemModel
Private _Add As ICommand
Private _DamagedItems As DamagedItemList
Public Property DamagedItem As DamagedItemModel
Get
Return _DamagedItem
End Get
Set(value As DamagedItemModel)
_DamagedItem = value
End Set
End Property
Public Property DamagedItems As DamagedItemList
Get
Return _DamagedItems
End Get
Set(value As DamagedItemList)
_DamagedItems = value
End Set
End Property
Public Property Add As ICommand
Get
Return _Add
End Get
Set(value As ICommand)
_Add = value
End Set
End Property
Public Sub New()
DamagedItem = New DamagedItemModel("", "", "")
DamagedItems = New DamagedItemList
Add = New DamagedItemAddEntryCommand(Me)
End Sub
Public Function CanUpdate() As Boolean
If DamagedItem.Description = "" Then Return False
If DamagedItem.Initiales = "" Then Return False
Return True
End Function
Public Sub AddEntry()
DamagedItems.Add(DamagedItem) 'Items get added to the datagrid
DamagedItem = New DamagedItemModel 'Does not seem to clear textboxes
End Sub
End Class
Here is how my XAML is set up:
<DataGrid ItemsSource="{Binding Path=DamagedItems}" AutoGenerateColumns="True" HorizontalAlignment="Stretch" Margin="12,90,12,0" Name="DataGrid1" VerticalAlignment="Top" Height="229" / >
<TextBox Text="{Binding DamagedItem.Description, UpdateSourceTrigger=PropertyChanged}" Height="23" HorizontalAlignment="Left" Margin="88,24,0,0" VerticalAlignment="Top" Width="249" />
<TextBox Text="{Binding DamagedItem.Initiales, UpdateSourceTrigger=PropertyChanged}" Height="23" HorizontalAlignment="Left" Margin="88,58,0,0" VerticalAlignment="Top" Width="249" />
As you can see, my textboxes are bound to my Model (which is contained in my ViewModel, which is bound to that Window's DataContext). Whenever I click on my "Add" button, whatever is in the textbox gets added to the DataGrid, but the content in the text boxes stay there.
This step is fine, I write in what I want to add and click on "Add"
After clicking on "Add" i get the following results in the DataGrid, which is fine. The issue is my text boxes are still filled with data yet the Model was cleared (see code after DamagedItemViewModel AddEntry method).
Now when I try to add the following text:
Description: "Part is bent"
Initiales: "A.C"
I get the following result:
The first letter typed in the description gets inputted in the first entry of the DataGrid, then it erases the text in the description textbox. Only then can I keep typing what I want. The same thing occurs for the initiales text box.
Any ideas? If you wish to see more of my code, suggest which portion I should add.
Thank you in advance!
Yup, I remember running into this one. You have to implement iNotifyPropertyCHnaged. This is how the viewmodel class "notifies" the user interface that there has been a change to the underlying property of a binding:
look here:
http://msdn.microsoft.com/en-us/library/ms743695.aspx
You will have to implement this for every property you want reflected back to the view. SO what I do is have a base viewmodel class (ViewModelBase which exposes method RasiePropertyChanged) which implements iNotifyPropertyChanged and then my viewmodles inherit from it. Then I notify the property changed in the property set of the property:
ie:
Public Property Selection As job
Get
Return Me._Selection
End Get
Set(ByVal value As job)
If _Selection Is value Then
Return
End If
_PreviousJob = _Selection
_Selection = value
RaisePropertyChanged(SelectionPropertyName)
End Set
End Property
This seems frustrating at first but is needed to keep the decoupling that MVVM supports. Its easy to implement.

How to add InputBindings to the Window after composition?

I am trying to master working with the MEF framework by implementing my own version of the well known Calculator example. The user interface is in WPF.
After composition the Viewmodel holds an ObservableCollection(Of IOperation) that is represented by a 'ListBox' of Buttons in the View. The text on each Button is a Char defined in IOperation as a property named Symbol. By binding the ListBox's SelectedItem to a property in the ViewModel I can just fire the Calculate method of the currently selected IOperation without knowing which Button was pressed. (Code illustrating this below.)
However, now I need to add InputBindings to the View , where each KeyBinding will be associated with the Symbol that is defined on the IOperation. It looks like that I cannot avoid implementing a Select Case(switch) statement to iterate through the Viewmodel's collection of IOperations to select the one on which the 'Calculate` method should be called.
Any ideas?
THE XAML:
<ListBox Grid.Column="1" Grid.Row="3" Name="OperationsList"
SelectedItem="{Binding ActiveOperation,Mode=TwoWay}"
ItemsSource="{Binding Operations}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid IsItemsHost="True"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Symbol}"
ToolTip="{Binding Description}"
Command="{Binding ElementName=OperationsList, Path=DataContext.ActivateOperation}"
Click="Button_Click_1"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
IOPERATION:
Public Interface IOperation
Function Calculate() As Double
Property Left As Double
Property Right As Double
Property Symbol As String
Property Description As String
End Interface
VIEWMODEL:
Private _activateOperation As Command
Public Property ActivateOperation As Command
Get
Return _activateOperation
End Get
Set(value As Command)
_activateOperation = value
OnPropertyChanged()
End Set
End Property
Public Property ActiveOperation As IOperation
Get
Return _compositor.ActiveOperation
End Get
Set(value As ICalculator)
_compositor.ActiveOperation = value
OnPropertyChanged()
End Set
End Property
Public ReadOnly Property Operations As ObservableCollection(Of IOperation)
Get
Return New ObservableCollection(Of ICalculator)(_compositor.Operations)
End Get
End Property
Private Sub DoActivateOperation(Optional parameter As Object = Nothing)
If Left.HasValue Then
MakeCalculation()
Else
Left = CDbl(Display)
ClearDisplay()
End If
End Sub
Code is a wee rough about the edges so adjust accordingly...
// in ViewModel, after composition
// map string symbols to input Keys (for brevity, using a Dictionary)
Dictionary<string, System.Windows.Input.Key> SymbolsToKeys = new Dictionary<string, Key>
{
{ "+", Key.Add },
// etc.
}
// compose the list (expose it via a public property)
// (ensure not to get confused: the KeyValuePair.Key is for the string symbol
// ... and the KeyValuePair.Value is for the Sys.Win.Input.Key value)
KeyBindings = (from symbolAndKey in SymbolsToKeys
join op in Operations on Equals(symbolAndKey.Key, op.Symbol)
select new KeyBinding(
new Command(op.Calculate)),
new KeyGesture(symbolAndKey.Value)
).ToList();
// in View, after Binding to ViewModel
var vm = DataContext as YourViewModel;
if(vm != null)
{
foreach(var keybinding in vm.KeyBindings){
this.InputBindings.Add(keybinding);
}
}

Deselecting ComboBoxItems in MVVM

I am using a standard wpf/mvvm application where i bind combo boxes to collections on a ViewModel.
I need to be able to de-select an item from the dropdown. Meaning, users should be able to select something, and later decide that they want to un-select it (select none) for it. the problem is that there are no empty elements in my bound collection
my initial thought was simply to insert a new item in the collection which would result having an empty item on top of the collection.
this is a hack though, and it affects all code that uses that collection on the view model.
for example if someone was to write
_myCollection.Frist(o => o.Name == "foo")
this will throw a null reference exception.
possible workaround is:
_myCollection.Where(o => o != null).First(o => o.Name == "foo");
this will work, but no way to ensure any future uses of that collection won't cause any breaks.
what's a good pattern / solution for being able to adding an empty item so the user can de-select. (I am also aware of CollectionView structure, but that seems like a overkill for something so simple)
Update
went with #hbarck suggestion and implemented CompositeCollection (quick proof of concept)
public CompositeCollection MyObjects {
get {
var col = new CompositeCollection();
var cc1 = new CollectionContainer();
cc1.Collection = _actualCollection;
var cc2 = new CollectionContainer();
cc2.Collection = new List<MyObject>() { null }; // PROBLEM
col.Add(cc2);
col.Add(cc1);
return col;
}
}
this code work with existing bindings (including SelectedItem) which is great.
One problem with this is, that if the item is completely null, the SelectedItem setter is never called upon selecting it.
if i modify that one line to this:
cc2.Collection = new List<MyObject>() { new MyObject() }; // PROBLEM
the setter is called, but now my selected item is just a basic initialized class instead of null.. i could add some code in the setter to check/reset, but that's not good.
I think the easiest way would be to use a CompositeCollection. Just append your collection to another collection which only contains the empty item (null or a placeholder object, whatever suites your needs), and make the CompositeCollection the ItemsSource for the ComboBox. This is probably what it is intended for.
Update:
This turns out to be more complicated than I first thought, but actually, I came up with this solution:
<Window x:Class="ComboBoxFallbackValue"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:t="clr-namespace:TestWpfDataBinding"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:w="clr-namespace:System.Windows;assembly=WindowsBase"
Title="ComboBoxFallbackValue" Height="300" Width="300">
<Window.Resources>
<t:TestCollection x:Key="test"/>
<CompositeCollection x:Key="MyItemsSource">
<x:Static Member="t:TestCollection.NullItem"/>
<CollectionContainer Collection="{Binding Source={StaticResource test}}"/>
</CompositeCollection>
<t:TestModel x:Key="model"/>
<t:NullItemConverter x:Key="nullItemConverter"/>
</Window.Resources>
<StackPanel>
<ComboBox x:Name="cbox" ItemsSource="{Binding Source={StaticResource MyItemsSource}}" IsEditable="true" IsReadOnly="True" Text="Select an Option" SelectedItem="{Binding Source={StaticResource model}, Path=TestItem, Converter={StaticResource nullItemConverter}, ConverterParameter={x:Static t:TestCollection.NullItem}}"/>
<TextBlock Text="{Binding Source={StaticResource model}, Path=TestItem, TargetNullValue='Testitem is null'}"/>
</StackPanel>
Basically, the pattern is that you declare a singleton NullInstance of the class you use as items, and use a Converter which converts this instance to null when setting the VM property. The converter can be written universally, like this (it's VB, I hope you don't mind):
Public Class NullItemConverter
Implements IValueConverter
Public Function Convert(value As Object, targetType As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.Convert
If value Is Nothing Then
Return parameter
Else
Return value
End If
End Function
Public Function ConvertBack(value As Object, targetType As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.ConvertBack
If value Is parameter Then
Return Nothing
Else
Return value
End If
End Function
End Class
Since you can reuse the converter, you can set this all up in XAML; the only thing that remains to be done in code is to provide the singleton NullItem.
Personally, I tend to add an "empty" version of whatever object is in my collection I'm binding to. So, for example, if you're binding to a list of strings, then in your viewmodel, insert an empty string at the beginning of the collection. If your Model has the data collection, then wrap it with another collection in your viewmodel.
MODEL:
public class Foo
{
public List<string> MyList { get; set;}
}
VIEW MODEL:
public class FooVM
{
private readonly Foo _fooModel ;
private readonly ObservableCollection<string> _col;
public ObservableCollection<string> Col // Binds to the combobox as ItemsSource
{
get { return _col; }
}
public string SelectedString { get; set; } // Binds to the view
public FooVM(Foo model)
{
_fooModel = model;
_col= new ObservableCollection<string>(_fooModel.MyList);
_col.Insert(0, string.Empty);
}
}
You could also extend the ComboBox to enable de-selecting. Add one or more hooks (eg, pressing the escape key) that allow the user to set the SelectedItem to null.
using System.Windows.Input;
public class NullableComboBox : ComboBox
{
public NullableComboBox()
: base()
{
this.KeyUp += new KeyEventHandler(NullableComboBox_KeyUp);
var menuItem = new MenuItem();
menuItem.Header = "Remove selection";
menuItem.Command = new DelegateCommand(() => { this.SelectedItem = null; });
this.ContextMenu = new ContextMenu();
this.ContextMenu.Items.Add(menuItem);
}
void NullableComboBox_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.Key == Key.Escape || e.Key == Key.Delete)
{
this.SelectedItem = null;
}
}
}
Edit Just noticed Florian GI's comment, the Context Menu might be another good deselect hook to add.
One option would be to create an adapter collection that you expose specifically for consumers that want an initial 'empty' element. You would need to create a wrapper class that implements IList (if you want same performance as with ObservableCollection) and INotifyCollectionChanged. You would need to listen to INotifyCollectionChanged on the wrapped collection, then rebroadcast the events with indices shifted up by one. All of the relevant list methods would also need to shift indices by one.
public sealed class FirstEmptyAdapter<T> : IList<T>, IList, INotifyCollectionChanged
{
public FirstEmptyCollection(ObservableCollection<T> wrapped)
{
}
//Lots of adapter code goes here...
}
Bare minimum if you want to avoid the IList methods is to implement INotifyCollectionChanged and IEnumerable<T>.
One simple approach is to re-template the ComboBox so that when there is an item select a small X appears on the right side of the box. Clicking that clears out the selected item.
This has the advantage of not making your ViewModels any more complicated

Resources