Is it possible to bind a list of objects? - c#

I have the following class:
public class MyClass
{
public MyClass()
{
OtherClass = new List<OtherClass>();
}
public List<OtherClass> OtherClass { get; set; }
}
OtherClass contains:
public class OtherClass
{
public OtherClass ()
{
}
public string Name { get; set; }
}
and the following xaml MyView:
<DataTemplate DataType="{x:Type Framework:MyClass}">
<StackPanel>
<Label FontSize="20" Content="{Binding Path=OtherClass.Name}" />
</StackPanel>
</DataTemplate>
in MyWindow referencing MyView:
<TabItem Header="My Class">
<Views:MyView DataContext="{Binding Path=MyClass}" />
</TabItem>
I have seen other examples of binding nested properties which suggest that Binding Path this way (ie, OtherClass.Name) works fine for a single object. However, I am binding a list of objects rather than a single object (in my example, a list of OtherClass).
Is it possible to bind a list of objects?

If you want to create DataTemplate for MyClass then you need to use some form of ItemsControl to display OtherClass list property
<DataTemplate DataType="{x:Type Framework:MyClass}">
<ItemsControl ItemsSource="{Binding OtherClass}" DisplayMemberPath="Name"/>
</DataTemplate>
also OtherClass.Name must be a public property and not private as it is at the moment
public class OtherClass
{
public OtherClass ()
{
}
public string Name { get; set; }
}
EDIT
DisplayMemberPath is the easiest way to display single property but if you want to display more then one property from OtherClass class, or change how it's formatted then you need to define ItemsControl.ItemTemplate instead and tell ItemsControl how to display each item
<ItemsControl ItemsSource="{Binding OtherClass}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<!-- more properties that you want to display -->
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

What you'll likely want to do here is make a ItemsControl, where each item of your OtherClass list will be prepresented by one item. Your ItemTemplate will dictate what each item in that list is to display, in your case the ItemTemplate will contain a Label which is bound to the Name property. See below:
<ItemsControl ItemsSource="{Binding OtherClass}" DataType="{x:Type Framework:MyClass}">
<!-- ItemTemplate -->
<ItemsControl.ItemTemplate>
<DataTemplate>
<Label Text="{Binding Name}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
(See a more complete example here)

If you are binding to a "list" of objects, the list must be bound to a control that has an ItemsSource property. These types of controls are able to bind to collections so that the inner DataContext of the control is the list's type. In your case, if you bind to the OtherClass property (the list), the DataContext scope will be of Type 'OtherClass' where you can then bind to its properties explicitly.
As others have mentioned, you can bind to an ItemsControl, but can also bind to a ListBox, ListView, DataGrid, TreeView, etc.
<!-- Using ItemsControl -->
<ItemsControl ItemsSource="{Binding OtherClass}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<DataTemplate.Resources>
<Style TargetType="TextBlock">
<Setter Property="FontSize" Value="20"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
</Style>
</DataTemplate.Resources>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Using ListBox-->
<ListBox ItemsSource="{Binding OtherClass}">
<ListBox.ItemTemplate>
<DataTemplate>
<DataTemplate.Resources>
<Style TargetType="TextBlock">
<Setter Property="FontSize" Value="20"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
</Style>
</DataTemplate.Resources>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

Related

Dynamically create ContextMenu

In my application I want to dynamically build up a context-menu. The first MenuItem is static and the second one should be a Separator. All items after the Separator are dynamically created at runtime.
I don't want to use code-behind becaus I'm working with the MVVM-Pattern.
My idea now was to create an interface called IAppMenuItem with the following three implementations
ModifyMenuItem (Static MenuItem)
SeparatorMenuItem
ExecuteMenuItem (Dynamic MenuItem
In the viewmodel of my application I've created an ObservableCollection<IAppMenuItem> which contains the ContextMenu-Items.
Until here everything works fine. My problem is the presentation of the ContextMenu-Items in the UI.
I tried to set the correct controls with the following DataTemplates in the Resources of the view.
<DataTemplate DataType="{x:Type model:SeparatorMenuItem}">
<Separator/>
</DataTemplate>
<DataTemplate DataType="{x:Type model:ModifyMenuItem}">
<MenuItem Header="Edit items"/>
</DataTemplate>
<DataTemplate DataType="{x:Type model:ExecuteMenuItem}">
<MenuItem Header="{Binding DisplayText}"/>
</DataTemplate>
The definition of my ContextMenu is just:
<ContextMenu ItemsSource="{Binding MenuItemsCollection}"/>
The DataTemplates are working fine, but the controls are drawn inside a MenuItem. So for example for the Separator I see in the UI a Separator-Control inside a MenuItem-Control. But I need the Separator to be the Control.
Anyone have an idea how to set the Controls inside the DataTemplates directly to the contextmenu?
Update:
The complete ContextMenu looks like:
<ToggleButton Margin="0,0,10,0"
AutomationProperties.Name="Update"
AutomationProperties.AutomationId="Update_List"
Content="Update"
AttachedProperties:ButtonExtensions.IsDropDownButton="True"
Style="{StaticResource GenericToggleButtonStyle}">
<ToggleButton.ContextMenu>
<controls:CustomContextMenu ItemsSource="{Binding MenuItemsCollection}">
<ContextMenu.Resources>
<ResourceDictionary>
<DataTemplate DataType="{x:Type model:ExecuteMenuItem}">
<MenuItem Header="{Binding DisplayText}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type model:ModifyMenuItem}">
<MenuItem Header="Edit items"/>
</DataTemplate>
</ResourceDictionary>
</ContextMenu.Resources>
</controls:CustomContextMenu>
</ToggleButton.ContextMenu>
</ToggleButton>
The GenericToggleButtonStyle is just:
<Style x:Key="GenericToggleButtonStyle" TargetType="ToggleButton"
BasedOn="{StaticResource {x:Type ToggleButton}}">
<Setter Property="MinWidth" Value="80" />
<Setter Property="Height" Value="22" />
<Setter Property="Padding" Value="3,1" />
</Style>
Here is a screenshot of the MenuItems
It seems when you set the ItemSource property on a ContextMenu the default ItemContainer used is MenuItem. This is why the Separator is rendered as a MenuItem.
You can fix this by implementing your own ContextMenu control that inherits from ContextMenu.
You need to override the IsItemItsOwnContainerOverride and GetContainerForItemOverride methods.
Please see my example below:
public class CustomContextMenu
: ContextMenu
{
private bool _mustGenerateAsSeparator = false;
protected override bool IsItemItsOwnContainerOverride(object item)
{
_mustGenerateAsSeparator = (item is SeparatorMenuItem);
return base.IsItemItsOwnContainerOverride(item);
}
protected override System.Windows.DependencyObject GetContainerForItemOverride()
{
if (_mustGenerateAsSeparator)
{
return new Separator { Style = this.FindResource(MenuItem.SeparatorStyleKey) as System.Windows.Style };
}
else
{
return base.GetContainerForItemOverride();
}
}
}
Style = this.FindResource(MenuItem.SeparatorStyleKey) as System.Windows.Style is needed as you have to apply the default MenuItem.SeparatorStyleKey style to get the default separator style.
This link pointed me into the right direction http://drwpf.com/blog/category/item-containers/
How to use the custom control in XAML:
window declaration :xmlns:cnt="your namespace"
<Label Content="Dynamic Menu">
<Label.ContextMenu>
<cnt:CustomContextMenu x:Name="contextMenu" ItemsSource="{Binding MenuItemsCollection}">
<ContextMenu.Resources>
<ResourceDictionary>
<DataTemplate DataType="{x:Type model:ExecuteMenuItem}">
<MenuItem Header="{Binding DisplayText}"></MenuItem>
</DataTemplate>
<DataTemplate DataType="{x:Type model:ModifyMenuItem}">
<MenuItem Header="{Binding DisplayText}"></MenuItem>
</DataTemplate>
</ResourceDictionary>
</ContextMenu.Resources>
</model:CustomContextMenu>
</Label.ContextMenu>
</Label>
Second Part of the question:
Update your data templates to use TextBlock as they will be rendered inside a MenuItem, please see below:
<DataTemplate DataType="{x:Type model:ModifyMenuItem}">
<TextBlock Header="Edit items"/>
</DataTemplate>
<DataTemplate DataType="{x:Type model:ExecuteMenuItem}">
<TextBlock Header="{Binding DisplayText}"/>
</DataTemplate>

How to correctly bind a ViewModel (which Include Separators) to WPF's Menu?

I'm using MVVM and I want to data bind my list of MenuViewModels to my maim menu. Which consists of a set of menu items and separators.
Here's my MenuItemViewModel code:
public interface IMenuItemViewModel
{
}
[DebuggerDisplay("---")]
public class SeparatorViewModel : IMenuItemViewModel
{
}
[DebuggerDisplay("{Header}, Children={Children.Count}")]
public class MenuItemViewModel : IMenuItemViewModel, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public MenuItemViewModel(string header, ICommand command, ImageSource imageSource)
{
Header = header;
Command = command;
ImageSource = imageSource;
Children = new List<IMenuItemViewModel>();
}
public string Header { get; private set; }
public ICommand Command { get; private set; }
public ImageSource ImageSource { get; private set; }
public IList<IMenuItemViewModel> Children { get; private set; }
}
And my Main window looks like this:
<Window.Resources>
<HierarchicalDataTemplate DataType="{x:Type ViewModel:MenuItemViewModel}"
ItemsSource="{Binding Children}">
<MenuItem Header="{Binding Header}"
Command="{Binding Command}"/>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type ViewModel:SeparatorViewModel}">
<Separator />
</DataTemplate>
</Window.Resources>
<DockPanel>
<Menu DockPanel.Dock="Top"
ItemsSource="{Binding MenuItems}">
</Menu>
</DockPanel>
Should be very simple stuff. Unfortunately, either the menu item looks wrong or the separator is an empty menuItem (depending on what I've tried).
So, how do I get my Menu to find my two DataTemplates?
Solved my own question
After spending several hours searching the web, I found lots of examples that work against the WPF's natural intentions but none that worked with it.
Here's how to work with the Menu control and not against it...
A little Background
WPF's Menu control will normally auto create MenuItem objects for you when it is binded to a POCO collection, using the ItemsSource property.
However, this default behavior can be overridden! Here's how...
The Solution
First, you must create a class that derives from ItemContainerTemplateSelector. Or use the simple class I've created:
public class MenuItemContainerTemplateSelector : ItemContainerTemplateSelector
{
public override DataTemplate SelectTemplate(object item, ItemsControl parentItemsControl)
{
var key = new DataTemplateKey(item.GetType());
return (DataTemplate) parentItemsControl.FindResource(key);
}
}
Second, you must add a reference to the MenuItemContainerTemplateSelector class to your Windows resources object, like so:
<Window.Resources>
<Selectors:MenuItemContainerTemplateSelector x:Key="_menuItemContainerTemplateSelector" />
Third, you must set two properties (UsesItemContainerTemplate, and ItemContainerTemplateSelector) on both the Menu and the MenuItem (which is defined in the HierarchicalDataTemplate).
Like so:
<HierarchicalDataTemplate DataType="{x:Type ViewModel:MenuItemViewModel}"
ItemsSource="{Binding Children}">
<MenuItem Header="{Binding Header}"
Command="{Binding Command}"
UsesItemContainerTemplate ="true"
ItemContainerTemplateSelector=
"{StaticResource _menuItemContainerTemplateSelector}"/>
</HierarchicalDataTemplate>
<Menu DockPanel.Dock="Top"
ItemsSource="{Binding MenuItems}"
UsesItemContainerTemplate="True"
ItemContainerTemplateSelector=
"{StaticResource _menuItemContainerTemplateSelector}">
</Menu>
Why it Works
For optimization purposes, the Menu uses the UsesItemContainerTemplate flag (which has a default value of false) to skip the DataTemplate lookup and just returns a normal MenuItem object. Therefore, we needed to set this value to true and then our ItemContainerTemplateSelector works as expected.
Happy Coding!
A solution without the TemplateSelector:
provide ItemContainerTemplates instead of the DataTemplates :
<ContextMenu ItemsSource="{Binding Path=MenuItems}" UsesItemContainerTemplate="True">
<ContextMenu.Resources>
<ResourceDictionary>
<ItemContainerTemplate DataType="{x:Type ViewModel:MenuItemViewModel }">
<MenuItem Header="{Binding Path=Header}" Command="{Binding Path=Command}" UsesItemContainerTemplate="True">
<MenuItem.Icon>
<Image Source="{Binding Path=ImageSource}"/>
</MenuItem.Icon>
</MenuItem>
</ItemContainerTemplate>
<ItemContainerTemplate DataType="{x:Type ViewModel:SeparatorViewModel}">
<Separator >
<Separator.Style>
<Style TargetType="{x:Type Separator}" BasedOn="{StaticResource ResourceKey={x:Static MenuItem.SeparatorStyleKey}}"/>
</Separator.Style>
</Separator>
</ItemContainerTemplate>
</ResourceDictionary>
</ContextMenu.Resources>
</ContextMenu>
Notes:
I haven't tried Children
the separator styled wrong: I had to manually re-apply the style
Another approach is to:
have a Boolean property on your menu item ViewModel that indicates whether an item is a separator or not
use a trigger based on this property to change the ControlTemplate of the MenuItem so that it uses a Separator control instead
Like so:
<Menu ItemsSource="{Binding MenuItems}">
<Menu.Resources>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Header" Value="{Binding Header}" />
<Setter Property="Command" Value="{Binding Command}" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsSeparator}" Value="True">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type MenuItem}">
<Separator />
</ControlTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
<HierarchicalDataTemplate DataType="{x:Type ViewModel:MenuItemViewModel}"
ItemsSource="{Binding Children}" />
</Menu.Resources>
</Menu>

How to properly position elements in a stack panel?

Let's say, I have a class that looks like this:
public class Item
{
public string Name { get; set; }
public List<Item> SubItems { get; set; }
}
I want to display a list of such items in such a way that top-level items are arranged horizontally, and their sub-elements are arranged vertically below them:
Item1 Item2 Item3 Item4
SubItem2.1
SubItem2.2
I'm trying to achieve that using a ListView, and my XAML looks like this:
<ListView x:Name="ItemsList">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical" VerticalAlignment="Top">
<TextBlock Text="{Binding Name}" Background="#FF8686FF" Width="25" Height="25"/>
<ListView ItemsSource="{Binding SubItems}" BorderBrush="{x:Null}">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
However, my list doesn't look exactly the way I want:
As you can see, the items are vertically aligned to the center of their enclosing stack panels (which, in turn, occupy all available space). How do I properly align elements to the top? Or maybe there is a better way to achieve what I want with different containers?
You have to set container style:
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="VerticalContentAlignment" Value="Top" />
</Style>
</ListView.ItemContainerStyle>

Item not changing group in a grouped listview

I have a listView in which I show a collection of Vehicles which are grouped by their MaintenanceState. If the MaintenanceState of the Vehicle updates I expect it to change group. The collection itself is correctly updated, however the view does not update accordingly. Below is some of my code, maybe someone can help me getting this to work.
This is my CollectionViewSource managing my groupings
<CollectionViewSource x:Key="GroupedVehicles" IsLiveGroupingRequested="True" Source="{Binding ItemCollection}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="MaintenanceState" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
Here is my ListView
<ListView ItemContainerStyle="{DynamicResource VehicleItemContainerStyle}"
ItemsSource="{Binding Source={StaticResource GroupedVehicles}}"
SelectedItem="{Binding SelectedItem}"
SelectionMode="Single"
Style="{DynamicResource VehiclesListViewStyle}">
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<StackPanel>
<Expander Header="{Binding Path=Name}"
IsExpanded="True"
Style="{DynamicResource VehicleListSectionExpanderStyle}">
<ItemsPresenter />
</Expander>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListView.GroupStyle>
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Number}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
This is what I do on my ViewModel
Vehicle updatedVehicle = new Vehicle(vehicleNumber, MaintenanceStateEnum.Running);
ItemCollection[index] = updatedVehicle;
The ItemCollection is of type ObservableCollection<Vehicle> and I make sure to only add, remove or replace Vehicles.
The MaintenanceStateEnum has the following values: InMaintenance, MarkedForMaintenance and Running.
This is what my Vehicle looks like
public class Vehicle
{
public Vehicle(int number, MaintenanceStateEnum state) {}
public int Number { get; private set; }
public MaintenanceStateEnum MaintenanceState { get; private set; }
}
So my problem:
If I have Vehicle(3, MaintenanceStateEnum.MarkedForMaintenace) and it is updated to Vehicle(3, MaintenanceStateEnum.InMaintenance) it does not change from the grouping MarkedForMaintenance to the grouping InMaintenance.
Interesting is that it does get removed from the MarkedForMaintenance grouping (the view even leaves a space as if the object is still there).
Does anyone know how I can fix my problem?
I think the issue here is that the view does not know that the collection has changed. You could try to change your container from ItemCollection to ObservableCollection which implements both INotifyCollectionChanged and INotifyPropertyChanged.

WPF binding to a UserControl property

I have a list of items, each of which contains a display property of a type inheriting from a user control. The idea is each inheriting class can decide for itself what it wants to display for the user. The items are themselves arranged in a DataTemplate for a tab control... something like...
<TabControl ItemsSource="{Binding FooList}">
<TabControl.ItemTemplate>
<DataTemplate>
<TabItem Header="{Binding Name}">
???
</TabItem>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
And the classes in the foo observablecollection look like...
public class IFoo
{
public String Name { get; set; }
public UserControl Display { get; set; }
...
}
What I can't figure out is how to add the display property where the ??? is in the XAML. Is there a way to do this (trying to avoid doing it from the code behind)?
What you need to do is use the ItemContainerStyle property of TabControl:
<TabControl ItemsSource="{Binding FooList}">
<TabControl.ItemContainerStyle>
<Style TargetType="{x:Type TabItem}">
<Setter Property="Header" Value="{Binding Name}" />
<Setter Property="Content" Value="{Binding Display}" />
</Style>
</TabControl.ItemContainerStyle>
</TabControl>

Categories