ListBox ItemTemplateSelector doesn't work - wpf

I am attempting to use an ItemTemplateSelector on a WPF ListBox and have looked at several examples online. Seemed simple enough but I cannot get it to work. I am hoping that someone can tell me where I've gone wrong:
Fist, I have an DataTemplateSelector class defined as follows:
Public Class DocketDataTemplateSelector
Inherits DataTemplateSelector
Public Overrides Function SelectTemplate(ByVal item As Object, ByVal container As DependencyObject) As DataTemplate
Return DataDocketHeaderTemplate
End Function
Private _DataDocketHeaderTemplate As DataTemplate
Public Property DataDocketHeaderTemplate() As DataTemplate
Get
Return _DataDocketHeaderTemplate
End Get
Set(ByVal value As DataTemplate)
_DataDocketHeaderTemplate = value
End Set
End Property
Private _DataDocketDataTemplate As DataTemplate
Public Property DataDocketDataTemplate() As DataTemplate
Get
Return _DataDocketDataTemplate
End Get
Set(ByVal value As DataTemplate)
_DataDocketDataTemplate = value
End Set
End Property
End Class
Very simple - just returns the DataDocketHeaderTemplate datatemplate for the time being until I can get it to work.
I then have my user control with the following as its resource definition:
<UserControl.Resources>
<DataTemplate x:Key="docketHeaderTemplate">
<TextBlock Text="Header Row Test" Background="Yellow"/>
</DataTemplate>
<DataTemplate x:Key="docketDataTemplate">
<TextBlock Text="Data Row Test" Background="Green"/>
</DataTemplate>
<local:DocketDataTemplateSelector DataDocketHeaderTemplate="{StaticResource docketHeaderTemplate}" DataDocketDataTemplate="{StaticResource docketDataTemplate}" x:Key="myDataTemplateSelector"/>
</UserControl.Resources>
The ListBox in the user control is simply defined like this:
<ListBox ItemsSource="{Binding TestData}" ItemTemplateSelector="{StaticResource myDataTemplateSelector}"/>
Then finally, my TestData list is defined in my bound viewmodel like so:
Private _listTestData As ObservableCollection(Of String) = Nothing
Public Property TestData As ObservableCollection(Of String)
Get
If _listTestData Is Nothing Then
_listTestData = New ObservableCollection(Of String)
_listTestData.Add("Row 1")
_listTestData.Add("Row 2")
_listTestData.Add("Row 3")
End If
Return _listTestData
End Get
Set(ByVal value As ObservableCollection(Of String))
_listTestData = value
NotifyPropertyChanged("TestData")
End Set
End Property
Now, I expect I would see a list of 3 rows in my listbox all saying 'Header Row Test' (since my datatemplateselector is always returning DataDocketHeaderTemplate). But instead I see my core data of
Row 1
Row 2
Row 3
This seems to indicate that my overriding datatemplateselector is not being hit (indeed if I set a breakpoint in DocketDataTemplateSelector, at no time do I see it being hit). Where am I going wrong with this?
Thanks

Sorry i can not post this as comment, i haven't got enough score.
I just tried your example code (my first VB project) and guess what, it works as expected: three times "Header Row Test" on yellow background. I've put the ListBox in a Grid in the UserControl, then put the UserControl in a Grid in a Window, then set the DataContext of the UserControl to a ViewModel object with your TestData property.
Something must be wrong that is not demonstrated by your example code, maybe you can provide more info.

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.

WPF - Binding between usercontrol

I have a user control (say A) with two ContentPresenter each binded to another usercontrol (say B and C).
In one of the two UserControl(say B) I have a listBox of MyItem.
<ListBox ItemsSource="{Binding MyItemList}" SelectedItem="{Binding SelectedMyItem}">
MyItem code:
Public Class MyItem
Implements INotifyPropertyChanged
Private m_Name As Integer
Public Property Name As Integer
Get
Return m_Name
End Get
Set(value As Integer)
m_Name = value
End Set
End Property
...
End Class
In the other one(say C) I have TextBoxes that I want to bind to SelectedItem properties(say MyItem.Name).
<TextBox Text="{Binding SelectedItem.Name}"/>
Is it possible in some way? Because I can't find one.
What you're saying is close to impossible.
Bind the textbox text to some property in view model c. Bind the list selected item to some property in view model b.
Emit an event with argument as the Name value as string when selected item is changed. Handle event in viewmodel c and update the textbox text with the received Name argument.
This would do the trick.
Another possible way is to create b and c view model instances in A viewmodel and update cviewmodelinstance.TextboxPropertyName when bviewmodelinstance.selectedListItem property is changed.

WPF datagrid - Update property after combo item selected

I'm new to WPF and have a question about an issue I'm having with a combobox on a datagrid. When an item is selected from the combo, the bound property does not update like I expect.
Since I can't post images (this is the first time I've posted to SO), I'll attempt to explain. If I select "D00120" from the combo list, the combo will reflect the change but the grid property does not get changed.
Xaml:
<DataGridTemplateColumn Header="CPT Code" Width="75">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding PartBCombo,
RelativeSource={RelativeSource AncestorType=Window}}"
DisplayMemberPath="PartBLookup_CPTCode"
SelectedValuePath="PartBLookup_ProcedureDescription"
SelectedValue="{Binding PartBBilling_ProcedureName, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
Property for combo items:
Partial Public Class PartBBilling_Combobox
Private _PartBLookup_CPTCode As String
Public Property PartBLookup_CPTCode As String
Get
Return _PartBLookup_CPTCode
End Get
Set(value As String)
_PartBLookup_CPTCode = value
End Set
End Property
Private _PartBLookup_ProcedureDescription As String
Public Property PartBLookup_ProcedureDescription As String
Get
Return _PartBLookup_ProcedureDescription
End Get
Set(value As String)
_PartBLookup_ProcedureDescription = value
End Set
End Property
End Class
Property for datagrid:
Private _PartBBilling_CPT As String
Public Property PartBBilling_CPT As String
Get
Return _PartBBilling_CPT
End Get
Set(value As String)
_PartBBilling_CPT = value
RaisePropertyChanged("PartBBilling_CPT")
End Set
End Property
Private _PartBBilling_ProcedureName As String
Public Property PartBBilling_ProcedureName As String
Get
Return _PartBBilling_ProcedureName
End Get
Set(value As String)
If _PartBBilling_ProcedureName <> value Then
_PartBBilling_ProcedureName = value
RaisePropertyChanged("PartBBilling_ProcedureName")
End If
End Set
End Property
When an item is selected, the UI displays correctly but the grid property does not get updated. Other than that, the combobox functions just fine.
Thanks for any help or advice.
First of i think to use something unique for your SelectedValuePath (used for identifying the correct selection)
DisplayMemberPath="PartBLookup_CPTCode"
SelectedValuePath="PartBLookup_CPTCode"
Also be careful and note that your SelectedValue will be bound to one of the values inside your PartBCombo collection whatever that type might be. So the Property you bind the SelectedValue to should be of that type.

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);
}
}

Resources