I'm trying to have a list from a combobox that highlights certain values, and the criteria for highlighting is a boolean. I have been able to make it work in testing by manually adding each combobox item manually and marking the tag, but I need it to be bound to be more dynamic. I've tried a couple different ways, but the dictionary seems like the simplest.
Dictionary
XAML:
<ComboBox Name="Box" HorizontalAlignment="Left" Margin="81,102,0,0" VerticalAlignment="Top" Width="120" ItemsSource="{Binding Items.Keys}">
<ComboBox.Resources>
<Style TargetType="{x:Type ComboBoxItem}">
<Style.Triggers>
<DataTrigger Binding="{Binding Items.Values}" Value="True">
<Setter Property="Background" Value="Yellow"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Resources>
</ComboBox>
Code-behind:
Public Module GlobalVariables
Public Item As New TestItem
End Module
Class MainWindow
Public Sub New()
InitializeComponent()
DataContext = Item
End Sub
End Class
Public Class TestItem
Public Property Items As New Dictionary(Of String, Boolean)
Public Sub New()
Items.Add("1", False)
Items.Add("2", True)
Items.Add("3", False)
Items.Add("4", False)
Items.Add("5", True)
End Sub
End Class
I'm guessing that my issue is that I'm using the collection of the dictionary values as the datatrigger binding rather than the individual one, but I'm not sure how to get the value associated with the key in XAML.
Thanks for any help
EDIT
Thanks to Tarazed's answer, I was able to get the test to work with this:
XAML:
<ComboBox Name="Box" HorizontalAlignment="Left" Margin="81,102,0,0" VerticalAlignment="Top" Width="120" ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedObject}" DisplayMemberPath="Name">
<ComboBox.Resources>
<Style TargetType="{x:Type ComboBoxItem}">
<Style.Triggers>
<DataTrigger Binding="{Binding Highlighted}" Value="True">
<Setter Property="Background" Value="Yellow"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Resources>
</ComboBox>
Code-behind:
Public Module GlobalVariables
Public TestCollection As New ItemCollection
End Module
Class MainWindow
Public Sub New()
InitializeComponent()
DataContext = TestCollection
End Sub
End Class
Public Class Item
Public Property Highlighted As Boolean
Public Property Name As String
End Class
Public Class ItemCollection
Public Property Items As New List(Of Item)
Public Property SelectedObject As Item
Public Sub New()
Items.Add(New Item With {.Name = "1", .Highlighted = False})
Items.Add(New Item With {.Name = "2", .Highlighted = True})
Items.Add(New Item With {.Name = "3", .Highlighted = False})
Items.Add(New Item With {.Name = "4", .Highlighted = False})
Items.Add(New Item With {.Name = "5", .Highlighted = True})
End Sub
End Class
You are correct, the binding is unable to associate the boolean value with the string because calling on .Keys and .Values produces two collectins.
Instead, take a look at the DisplayMemberPath of the ItemsControl (ComboBox). This allows you to pick the property of a complex object to be displayed by the control. Using this you can create a collection, rather than a dictionary, of instances of a custom structure or class containing the name and highlight boolean and choose the name for display.
Related
Please see the following code.
It creates a ListBox with five items. The selected item of the ListBox is colored in yellow, previous items (index below selected index) are colored in green and future items (index above selected index) are colored in red.
ItemViewModel.vb
Public Class ItemViewModel
Implements INotifyPropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Private _title As String
Private _isOld As Boolean
Private _isNew As Boolean
Protected Overridable Sub OnPropertyChanged(<CallerMemberName> Optional propertyName As String = Nothing)
If String.IsNullOrEmpty(propertyName) Then
Exit Sub
End If
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
Public Property Title As String
Get
Return _title
End Get
Set(value As String)
_title = value
Me.OnPropertyChanged()
End Set
End Property
Public Property IsOld As Boolean
Get
Return _isOld
End Get
Set(value As Boolean)
_isOld = value
Me.OnPropertyChanged()
End Set
End Property
Public Property IsNew As Boolean
Get
Return _isNew
End Get
Set(value As Boolean)
_isNew = value
Me.OnPropertyChanged()
End Set
End Property
End Class
MainViewModel:
Public Class MainViewModel
Implements INotifyPropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Private ReadOnly _items As ObservableCollection(Of ItemViewModel)
Private _selectedIndex As Integer
Public Sub New()
_items = New ObservableCollection(Of ItemViewModel)
_items.Add(New ItemViewModel With {.Title = "Very old"})
_items.Add(New ItemViewModel With {.Title = "Old"})
_items.Add(New ItemViewModel With {.Title = "Current"})
_items.Add(New ItemViewModel With {.Title = "New"})
_items.Add(New ItemViewModel With {.Title = "Very new"})
Me.SelectedIndex = 0
End Sub
Protected Overridable Sub OnPropertyChanged(<CallerMemberName> Optional propertyName As String = Nothing)
If String.IsNullOrEmpty(propertyName) Then
Exit Sub
End If
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
Public ReadOnly Property Items As ObservableCollection(Of ItemViewModel)
Get
Return _items
End Get
End Property
Public Property SelectedIndex As Integer
Get
Return _selectedIndex
End Get
Set(value As Integer)
_selectedIndex = value
Me.OnPropertyChanged()
For index As Integer = 0 To Me.Items.Count - 1
Me.Items(index).IsOld = (index < Me.SelectedIndex)
Me.Items(index).IsNew = (index > Me.SelectedIndex)
Next index
End Set
End Property
End Class
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:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="300" Width="200">
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<ListBox ItemsSource="{Binding Items}" SelectedIndex="{Binding SelectedIndex}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Title}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsOld}" Value="True">
<Setter Property="Foreground" Value="Green" />
</DataTrigger>
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}" Value="True">
<Setter Property="Foreground" Value="Yellow" />
</DataTrigger>
<DataTrigger Binding="{Binding IsNew}" Value="True">
<Setter Property="Foreground" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Window>
This works like expected, but I don't like, that the ItemViewModel holds the properties IsOld and IsNew and that the MainViewModel is responsible for updating these properties. In my opinion that should be done by the ListBox, not by every view model that might be the DataContext for my ListBox.
I already tried to create two attached properties for ListBoxItem and bind to them (like I bound to IsSelected for the current item). But I couldn't figure out an event on which I update those attached properties.
Is using these attached properties the way to go? When and/or where do I update those attached properties?
I tried to attach to the ValueChanged event of the ItemsSource property of the ListBox to be able to attach to the CollectionChanged event of the underlying collection. But I failed getting the ListBoxItem for an item, since these containers are created asynchronously (so I assume). And since the ListBox uses a VirtualizingStackPanel by default, I wouldn't get a ListBoxItem for every item of my underlying collection anyway.
Please keep in mind that the collection of items I bind to is observable and can change. So the IsOld and IsNew properties have to be updated whenever the source collection itself changes, whenever the content of the source collection changes and whenever the selected index changes.
Or how else can I achieve what I like to achieve?
I didn't flag VB.net on purpose since the question doesn't have anything to do with VB.net and I'm fine with answers in C# as well.
Thank you.
One way you can achieve this is through an attached behavior. This allows you to keep the display behavior with the ListBox and away from your view-model, etc.
First, I created an enum to store the states of the items:
namespace WpfApp4
{
public enum ListBoxItemAge
{
Old,
Current,
New,
None
}
}
Next, I created an attached behavior class with two attached properties:
IsActive (bool) = Turns on the behavior for the ListBox
ItemAge (ListBoxItemAge) = Determines if an item should be displayed in Red, Yellow, Green, etc.
When IsActive is set to True on a ListBox, it will subscribe to the SelectionChanged event and will handle setting each ListBoxItems age.
Here is the code:
using System.Windows;
using System.Windows.Controls;
namespace WpfApp4
{
public class ListBoxItemAgeBehavior
{
#region IsActive (Attached Property)
public static readonly DependencyProperty IsActiveProperty =
DependencyProperty.RegisterAttached(
"IsActive",
typeof(bool),
typeof(ListBoxItemAgeBehavior),
new PropertyMetadata(false, OnIsActiveChanged));
public static bool GetIsActive(DependencyObject obj)
{
return (bool)obj.GetValue(IsActiveProperty);
}
public static void SetIsActive(DependencyObject obj, bool value)
{
obj.SetValue(IsActiveProperty, value);
}
private static void OnIsActiveChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is ListBox listBox)) return;
if ((bool) e.NewValue)
{
listBox.SelectionChanged += OnSelectionChanged;
}
else
{
listBox.SelectionChanged -= OnSelectionChanged;
}
}
private static void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var listBox = (ListBox) sender;
var selectedIndex = listBox.SelectedIndex;
SetItemAge(listBox.ItemContainerGenerator.ContainerFromIndex(selectedIndex), ListBoxItemAge.Current);
foreach (var item in listBox.ItemsSource)
{
var index = listBox.Items.IndexOf(item);
if (index < selectedIndex)
{
SetItemAge(listBox.ItemContainerGenerator.ContainerFromIndex(index), ListBoxItemAge.Old);
}
else if (index > selectedIndex)
{
SetItemAge(listBox.ItemContainerGenerator.ContainerFromIndex(index), ListBoxItemAge.New);
}
}
}
#endregion
#region ItemAge (Attached Property)
public static readonly DependencyProperty ItemAgeProperty =
DependencyProperty.RegisterAttached(
"ItemAge",
typeof(ListBoxItemAge),
typeof(ListBoxItemAgeBehavior),
new FrameworkPropertyMetadata(ListBoxItemAge.None));
public static ListBoxItemAge GetItemAge(DependencyObject obj)
{
return (ListBoxItemAge)obj.GetValue(ItemAgeProperty);
}
public static void SetItemAge(DependencyObject obj, ListBoxItemAge value)
{
obj.SetValue(ItemAgeProperty, value);
}
#endregion
}
}
The XAML looks something like this. This is just a simple example:
<ListBox
local:ListBoxItemAgeBehavior.IsActive="True"
ItemsSource="{Binding Data}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Title}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=(local:ListBoxItemAgeBehavior.ItemAge), RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Value="Old">
<Setter Property="Foreground" Value="Red" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=(local:ListBoxItemAgeBehavior.ItemAge), RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Value="Current">
<Setter Property="Foreground" Value="Yellow" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=(local:ListBoxItemAgeBehavior.ItemAge), RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Value="New">
<Setter Property="Foreground" Value="Green" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I've created three DataTriggers that look for the value of the ListBoxItemAgeBehavior.ItemAge and then set the appropriate Foreground color. Since the attached property is set on the ListBoxItem, I'm doing a RelativeSource on the binding.
I hope this helps.
I'd like to be able to bind the ItemsSource of a ContextMenu to a Collection in my view model, show Separators in the ContextMenu, and the ItemsSource has to be hierarchical (every level of the hierarchy will look the same).
In one of my other questions I managed to be able to show menu items and separators in a data bound ContextMenu, but now I struggle with making the ItemsSource hierarchical.
Right now I don't know what's going on, maybe you can enlighten me?
Here's my code again (simplified to be short, but working):
MenuItemViewModel.vb
Public Class MenuItemViewModel
Implements ICommand
Public Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged
Public Property IsSeparator As Boolean
Public Property Caption As String
Private ReadOnly _subItems As List(Of MenuItemViewModel)
Public Sub New(createItems As Boolean, level As Byte)
_subItems = New List(Of MenuItemViewModel)
If createItems Then
_subItems.Add(New MenuItemViewModel(level < 4, level + 1) With {.Caption = "SubItem 1"})
_subItems.Add(New MenuItemViewModel(False, level + 1) With {.IsSeparator = True, .Caption = "SubSep 1"})
_subItems.Add(New MenuItemViewModel(level < 4, level + 1) With {.Caption = "SubItem 2"})
End If
End Sub
Public ReadOnly Property SubItems As List(Of MenuItemViewModel)
Get
Return _subItems
End Get
End Property
Public ReadOnly Property Command As ICommand
Get
Return Me
End Get
End Property
Public Sub Execute(ByVal parameter As Object) Implements ICommand.Execute
MessageBox.Show(Me.Caption)
End Sub
Public Function CanExecute(ByVal parameter As Object) As Boolean Implements ICommand.CanExecute
Return True
End Function
End Class
The view model for each menu item on each level has a Caption to show in the context menu, an IsSeparator flag to indicate whether it's a separator or a functional menu item, a Command to be bound to when being a functional menu item and of course a SubItems collection containing functional menu items and separators down to a certain hierarchy level.
MainViewModel.vb
Public Class MainViewModel
Private ReadOnly _items As List(Of MenuItemViewModel)
Public Sub New()
_items = New List(Of MenuItemViewModel)
_items.Add(New MenuItemViewModel(True, 0) With {.Caption = "Item 1"})
_items.Add(New MenuItemViewModel(False, 0) With {.IsSeparator = True, .Caption = "Sep 1"})
_items.Add(New MenuItemViewModel(True, 0) With {.Caption = "Item 2"})
_items.Add(New MenuItemViewModel(True, 0) With {.Caption = "Item 3"})
_items.Add(New MenuItemViewModel(False, 0) With {.IsSeparator = True, .Caption = "Sep 2"})
_items.Add(New MenuItemViewModel(True, 0) With {.Caption = "Item 4"})
End Sub
Public ReadOnly Property Items As List(Of MenuItemViewModel)
Get
Return _items
End Get
End Property
End Class
The main view model does only have an Items collection containing functional menu items as well as separators.
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:WpfApp3"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance local:MainViewModel, IsDesignTimeCreatable=True}"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<Window.Resources>
<ControlTemplate x:Key="mist" TargetType="{x:Type MenuItem}">
<Separator />
</ControlTemplate>
<ControlTemplate x:Key="mict" TargetType="{x:Type MenuItem}">
<MenuItem Header="{Binding Caption}" Command="{Binding Command}" ItemsSource="{Binding SubItems}" />
</ControlTemplate>
<Style x:Key="cmics" TargetType="{x:Type MenuItem}">
<Setter Property="Template" Value="{StaticResource mict}" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsSeparator}" Value="True">
<Setter Property="Template" Value="{StaticResource mist}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<TextBox HorizontalAlignment="Center" VerticalAlignment="Center" Text="Right click me">
<TextBox.ContextMenu>
<ContextMenu ItemsSource="{Binding Items}" ItemContainerStyle="{StaticResource cmics}">
<ContextMenu.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:MenuItemViewModel}" ItemsSource="{Binding SubItems}" />
</ContextMenu.ItemTemplate>
</ContextMenu>
</TextBox.ContextMenu>
</TextBox>
</Grid>
</Window>
The window resources contain two ControlTemplates "mist" and "mict" and a Style "cmics" that switches between the two ControlTemplates depending on the value of the IsSeparator flag. This works fine as long as the ItemsSource is not hierarchical (see my other question).
If my Style "cmics" is attached to the ItemContainerStyle of the ContextMenu only (as in my example code) then it looks like this:
The first level works but the others don't. This doesn't change when attaching my Style "cmics" to the ItemContainerStyle of the HierarchicalDataTemplate as well.
If I only attach my Style "cmics" to the HierarchicalDataTemplate then it looks like this:
The first level doesn't show captions and separators, the second level works and the other levels don't work.
So, how can I persuade the ContextMenu to use my Style "cmics" as the ItemContainerStyle for every hierarchy level?
I just did some changes to your (TextBox) in Xaml portion. Have a look at this,
<TextBox HorizontalAlignment="Center" VerticalAlignment="Center" Text="Right click me">
<TextBox.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:MenuItemViewModel}" ItemsSource="{Binding SubItems}">
<Button Content="{Binding Caption}" Command="{Binding Command}" Background="Red"/>
</HierarchicalDataTemplate>
</TextBox.Resources>
<TextBox.ContextMenu>
<ContextMenu ItemsSource="{Binding Items}" ItemContainerStyle="{StaticResource cmics}"/>
</TextBox.ContextMenu>
</TextBox>
Basically I have removed your ContexttMenu ItemTemplate and added under TextBox.Resources a hierarchical data template.
Inside the data template, I just now added a radio button. You can change the content as per your needs.
Let me know if this solves your problem or else needed any other help.
I found an answer here.
I had to create an empty view model just for the separators and a class that derives from ItemContainerTemplateSelector to return the DataTemplate that belongs to the type of the menu item ("MenuItemViewModel" or "SeparatorViewModel").
The linked article should be self explanatory.
Building my first project with MVVM, I would like to know if the following code breaks the model.
The aim of the command is to switch between two UserControls (ucDrumStandard and ucDrumStandardList. Both share the same viewmodel.
This viewmodel inherits from a viewmodelbase containing the "ParentContext".
UserControls are 'stored' in ParentContext.listOfViews.
(Apologize for my VB Code ;-)
#Region "CmdSwitchDrumStandardView"
Public ReadOnly Property CmdSwitchDrumStandardView() As ICommand
Get
If _cmdSwitchDrumStandardView Is Nothing Then
_cmdSwitchDrumStandardView = New RelayCommand(AddressOf SwitchDrumStandardView)
End If
Return _cmdSwitchDrumStandardView
End Get
End Property
Private Sub SwitchDrumStandardView()
' There are two views(ucDrumStandard and ucDrumStandardList) for the same viewmodel
If ParentContext.CurrentView.Uid = "ucDrumStandard" Then
' switch to ucDrumStandardGrid
If ParentContext.ListOfViews.ContainsKey("ucDrumStandardGrid") Then
ParentContext.CurrentView = (From obj As KeyValuePair(Of String, UIElement) In ParentContext.ListOfViews
Where obj.Key = "ucDrumStandardGrid"
Select obj.Value).FirstOrDefault
Else
Dim m_ucDrumStandardGrid = New ucDrumStandardGrid
ParentContext.ListOfViews.Add("ucDrumStandardGrid", m_ucDrumStandardGrid)
ParentContext.CurrentView = m_ucDrumStandardGrid
End If
ElseIf ParentContext.CurrentView.Uid = "ucDrumStandardGrid" Then
' switch to ucDrumStandard
If ParentContext.ListOfViews.ContainsKey("ucDrumStandard") Then
ParentContext.CurrentView = (From obj As KeyValuePair(Of String, UIElement) In ParentContext.ListOfViews
Where obj.Key = "ucDrumStandard"
Select obj.Value).FirstOrDefault
Else
Dim m_ucDrumStandard = New ucDrumStandard
ParentContext.ListOfViews.Add("ucDrumStandard", m_ucDrumStandard)
ParentContext.CurrentView = m_ucDrumStandard
End If
End If
End Sub
#End Region
In the MVVM pattern the communication between the ViewModel & View layers should be done only by Binding & Commands. ViewModel code shouldn't use FrameworkElements so yes, your code "break" the MVVM pattern.
Your ParentContext which I assume is a ViewModel class should have a CurrentView property that's of type of the ViewModel layer of the "Page" objects.
The render of those ViewModel objects in the View layer should be done using DataTemplate & Bindings. I'll add a little sample illustrating all this.
//ViewModel
public class AppVM : INotifyPropertyChanged {
//your code...
private PageVM _currentView;
public PageVM CurrentView {
get {return _currentView;}
set {
_currentView = value;
OnPropertyChanged("CurrentView");
}
}
}
public class PageVM : INotifyPropertyChanged {
//your "view" data visible in the UI
}
//XAML
<ContentControl Content="{Binding CurrentView, Mode=OneWay}">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type ucDrumStandardVM}">
<ucDrumStandard/>
</DataTemplate>
<DataTemplate DataType="{x:Type ucDrumStandardListVM}">
<ucDrumStandardList/>
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
If you really want to keep the same ViewModel object for your two "pages" then you have to use a Trigger
<ContentControl Content="{Binding CurrentView, Mode=OneWay}">
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}">
<Style.Triggers>
<DataTrigger Binding="{Binding CurrentView.Uid, Mode=OneWay}" Value="ucDrumStandard">
<Setter Property="ContentTemplate">
<DataTemplate>
<ucDrumStandard/>
</DataTemplate>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding CurrentView.Uid, Mode=OneWay}" Value="ucDrumStandardGrid">
<Setter Property="ContentTemplate">
<DataTemplate>
<ucDrumStandardList/>
</DataTemplate>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Resources>
</ContentControl>
I have a comboxbox that is databound to an observablecollection from my viewmodel. I can get my list to populate with the data but I also would like to add a default item like "--All Models--". The code below displays "--All Models--" as the default item but it is not selectable if you select another item.
<ContentControl Content="{Binding Items}">
<ContentControl.ContentTemplate>
<DataTemplate>
<Grid>
<ComboBox x:Name="cb" ItemsSource="{Binding}"/>
<TextBlock x:Name="tb" Text="--Choose One--" IsHitTestVisible="False" Visibility="Hidden"/>
</Grid>
<DataTemplate.Triggers>
<Trigger SourceName="cb" Property="SelectedItem" Value="{x:Null}">
<Setter TargetName="tb" Property="Visibility" Value="Visible"/>
</Trigger>
</DataTemplate.Triggers>
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
I have tried with a compositecollection but that does not seem to to work. Is there a way to accomplish this?
Thanks in advance!
CompositeCollection should work, if you know how to use it that is; one important thing about it is that it does not inherit a DataContext, this means you need to reference your source in some other manner, further if that method is x:Reference you may not create a cyclic reference, this can be avoided by placing the collection in the Resources of the element referenced. e.g.
<Window.Resources>
<CompositeCollection x:Key="compCollection">
<ComboBoxItem Content="-- All Models --"/>
<CollectionContainer Collection="{Binding MyCollection, Source={x:Reference Window}}"/>
</CompositeCollection>
...
</Window.Resources>
You can then just use this via ItemsSource="{StaticResource compCollection}".
Build the view interaction logic into the viewmodel. my suggestion make the Observable collection type a viewmodel that is populated by the source list, plus one more viewmodel for the "not selected" item.
something like
public class ItemViewModel
{
public string Description { get; set; }
public int Id { get; set; }
}
public class ViewModel : ViewModelBase
{
public ObservableCollection<ItemViewModel> Items { get; set; } // Bound to ContentControl
private void Init()
{
Items = new ObservableCollection<ItemViewModel>();
Items.Add(new ItemViewModel() { Description = "--choice one--" , Id = null });
Items.AddRange(Model.Items.Select(i=> new ItemViewModel() { Description = i.Description , Id = i.Id }));
}
}
Then you can process SelectedItem's Id with the null sematic.
You can change your collection generic type to object and add there --All Models-- thing.
how can I bind the Content of a ContentControl to an ObservableCollection.
The control should show an object as content only if the ObservableColelction contains exactly one object (the object to be shown).
Thanks,
Walter
This is easy. Just use this DataTemplate:
<DataTemplate x:Key="ShowItemIfExactlyOneItem">
<ItemsControl x:Name="ic">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate><Grid/></ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Count}" Value="1">
<Setter TargetName="ic" Property="ItemsSource" Value="{Binding}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
This is used as the ContentTemplate of your ContentControl. For example:
<Button Content="{Binding observableCollection}"
ContentTemplate="{StaticResource ShowItemIfExactlyOneItem}" />
That's all you need to do.
How it works: The template normally contains an ItemsControl with no items, which is invisible and has no size. But if the ObservableCollection that is set as Content ever has exactly one item in it (Count==1), the trigger fires and sets the ItemsSource of the ItmesControl, causing the single item to display using a Grid for a panel. The Grid template is required because the default panel (StackPanel) does not allow its content to expand to fill the available space.
Note: If you also want to specify a DataTemplate for the item itself rather than using the default template, set the "ItemTemplate" property of the ItemsControl.
+1, Good question :)
You can bind the ContentControl to an ObservableCollection<T> and WPF is smart enough to know that you are only interested in rendering one item from the collection (the 'current' item)
(Aside: this is the basis of master-detail collections in WPF, bind an ItemsControl and a ContentControl to the same collection, and set the IsSynchronizedWithCurrentItem=True on the ItemsControl)
Your question, though, asks how to render the content only if the collection contains a single item... for this, we need to utilize the fact that ObservableCollection<T> contains a public Count property, and some judicious use of DataTriggers...
Try this...
First, here's my trivial Model object, 'Customer'
public class Customer
{
public string Name { get; set; }
}
Now, a ViewModel that exposes a collection of these objects...
public class ViewModel
{
public ViewModel()
{
MyCollection = new ObservableCollection<Customer>();
// Add and remove items to check that the DataTrigger fires correctly...
MyCollection.Add(new Customer { Name = "John Smith" });
//MyCollection.Add(new Customer { Name = "Mary Smith" });
}
public ObservableCollection<Customer> MyCollection { get; private set; }
}
Set the DataContext in the Window to be an instance of the VM...
public Window1()
{
InitializeComponent();
this.DataContext = new ViewModel();
}
and here's the fun bit: the XAML to template a Customer object, and set a DataTrigger to remove the 'Invalid Count' part if (and only if) the Count is equal to 1.
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
Title="Window1" Height="300" Width="300">
<Window.Resources>
<Style TargetType="{x:Type ContentControl}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate x:Name="template">
<Grid>
<Grid Background="AliceBlue">
<TextBlock Text="{Binding Name}" />
</Grid>
<Grid x:Name="invalidCountGrid" Background="LightGray" Visibility="Visible">
<TextBlock
VerticalAlignment="Center" HorizontalAlignment="Center"
Text="Invalid Count" />
</Grid>
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Count}" Value="1">
<Setter TargetName="invalidCountGrid" Property="Visibility" Value="Collapsed" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<ContentControl
Margin="30"
Content="{Binding MyCollection}" />
</Window>
UPDATE
To get this dynamic behaviour working, there is another class that will help us... the CollectionViewSource
Update your VM to expose an ICollectionView, like:
public class ViewModel
{
public ViewModel()
{
MyCollection = new ObservableCollection<Customer>();
CollectionView = CollectionViewSource.GetDefaultView(MyCollection);
}
public ObservableCollection<Customer> MyCollection { get; private set; }
public ICollectionView CollectionView { get; private set; }
internal void Add(Customer customer)
{
MyCollection.Add(customer);
CollectionView.MoveCurrentTo(customer);
}
}
And in the Window wire a button Click event up to the new 'Add' method (You can use Commanding if you prefer, this is just as effective for now)
private void Button_Click(object sender, RoutedEventArgs e)
{
_viewModel.Add(new Customer { Name = "John Smith" });
}
Then in the XAML, without changing the Resource at all - make this the body of your Window:
<StackPanel>
<TextBlock Height="20">
<TextBlock.Text>
<MultiBinding StringFormat="{}Count: {0}">
<Binding Path="MyCollection.Count" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<Button Click="Button_Click" Width="80">Add</Button>
<ContentControl
Margin="30" Height="120"
Content="{Binding CollectionView}" />
</StackPanel>
So now, the Content of your ContentControl is the ICollectionView, and you can tell WPF what the current item is, using the MoveCurrentTo() method.
Note that, even though ICollectionView does not itself contain properties called 'Count' or 'Name', the platform is smart enough to use the underlying data source from the CollectionView in our Bindings...