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.
Related
I have the following problem with my calculator app which I'm doing in the MVVM pattern.
I'm redoing the Windows 10 Calculator in Standard Mode. I made an ObservableCollection of MemoryItem.
MemoryItem is a class that contains an int for the Index, a double for the value and a RelayCommand for the MemoryButtons.
Basically it looks like this and is connected to my ViewModel:
public class MemoryItem
{
public double MemoryItemValue { get; set; }
public int SelectedMemoryItemIndex { get; set; }
public RelayCommand MemoryItemChange { get; set; }
}
So I've binded the SelectedMemoryItemIndex Property to the SelectedItemIndex in WPF.
My ListBox looks like this:
<ListBox Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Style="{StaticResource MemoryListBoxStyle}"
Visibility="{Binding MemoryVisibility}" ItemsSource="{Binding MemoryCollection}"
SelectedItem="{Binding SelectedMemoryItem}" SelectionMode="Extended" SelectedIndex="{Binding SelectedMemoryItemIndex}"
HorizontalContentAlignment="Right"/>
While the style of it looks like this:
<Style x:Key="MemoryListBoxStyle" TargetType="ListBox">
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<UniformGrid Rows="2" Margin="5">
<TextBlock Style="{StaticResource DisplayStyle}" Text="{Binding MemoryItemValue}" FontSize="20"/>
<DockPanel LastChildFill="False">
<Button Content="MC" Style="{StaticResource MemoryItemButton}"
Command="{Binding MemoryItemChange}" CommandParameter="{x:Static md:MemoryUsage.Clear}"/>
<Button Content="M+" Style="{StaticResource MemoryItemButton}"
Command="{Binding MemoryItemChange}" CommandParameter="{x:Static md:MemoryUsage.Add}"/>
<Button Content="M-" Style="{StaticResource MemoryItemButton}"
Command="{Binding MemoryItemChange}" CommandParameter="{x:Static md:MemoryUsage.Substract}"/>
</DockPanel>
</UniformGrid>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
The bindings work BUT I don't know how to have the new MemoryItem selected after Inserting the new MemoryItem and deleting the new one. Is there a better of way inserting the new item? ObservableCollection doesn't include a method to update a specific item (as far as I know).
This is the method I'm using to add the value to the MemoryItemValue and insert it in my Collection:
case MemoryUsage.Add:
if (SelectedMemoryItemIndex == -1)
{
SelectedMemoryItemIndex = 0;
}
MemoryItemValue += Eingabe1;
MemoryCollection.Insert(SelectedMemoryItemIndex +1, MItem);
MemoryCollection.RemoveAt(SelectedMemoryItemIndex);
break;
This way it worked but I always have to select the new inserted MemoryItem.
I'm thankful for ANY help provided by you.
Please keep in mind that I'm a beginner in programming and this is my first SO question ever.
Here is a post that helps answer this question.
But basically:
Create an IsSelected property on your MemoryItem class and bind ListBoxItem.IsSelected to that property.
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</ListBox.ItemContainerStyle>
When you want your new item selected, just set IsSelected to true.
IsSelected = true;
And shazam! It should work.
Here is code copied from another answer that may give you more information.
<ListBox ItemsSource="{Binding Items, Source={StaticResource ViewModel}}"
SelectionMode="Extended">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding IsItemSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ItemText}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Forgive me for leaving that example exactly as I found it.
Scenario: a datagrid to show custom events from custom sources.
The grid must group rows by source, ordering events by datetime descending (newer always on top). No manual sorting allowed, virtualization enabled, MVVM pattern.
Here's my issue: the group header itself must follow the datetime ordering, that is the group containg the latest event must scale to top.
ViewModel:
public class Event
{
public Int32 Source_Id { get; set; }
public String Source_Name { get; set; }
public DateTime Event_DateTime { get; set; }
}
CollectionView is grouped by Source_Id and sorted by Event_DateTime:
<CollectionViewSource x:Key='alarms_src' Source="{Binding Alarms}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Source_Id" />
</CollectionViewSource.GroupDescriptions>
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Event_DateTime" Direction="Descending" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
GroupStyle is a simple expander with source name and latest event datetime into the header:
<DataGrid.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Margin" Value="4"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<Expander IsExpanded="True" Background="LightGray">
<Expander.Header>
<StackPanel Orientation="Horizontal" Background="Transparent">
<TextBlock Margin="8" Text="{Binding Items[0].Source_Name}"/>
<TextBlock Margin="8" Text="{Binding Items[0].Event_DateTime}"/>
</StackPanel>
</Expander.Header>
<Expander.Content>
<ItemsPresenter/>
</Expander.Content>
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</DataGrid.GroupStyle>
Output:
simplified grid
The red arrows in the image points to the 4th row I've generated, with the latest timestamp.
The grid is correctly grouping and sorting INSIDE each group, but what I expect is that the Device_1 group would scale on top of the grid: how to obtain this?
Update: wetransfer link to sample project
CollectionViewSource.GetDefaultView(testdg.ItemsSource).Refresh();
Here testdg is my DataGrid Control name.
What i thought is to refresh the itemsource of the datagrid and the rest sorting will be taken care. Checkout the method.
private void addEvent(Int32 source)
{
Event a = new Event();
a.Source_Id = source;
a.Source_Name = "Device_" + source.ToString();
a.Event_DateTime = DateTime.Now;
this.Alarms.Add(a);
CollectionViewSource.GetDefaultView(testdg.ItemsSource).Refresh();
}
Sorry, misunderstood your question previously.
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>
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>
I am hoping that this is something simple and have just missed something obvious. I am using MVVM and have a Datagrid which is bound to a CollectionViewSource this in turn is populated with an ObservableCollection, the ObservableCollection is initally unpopulated and added to by way of tick boxes on the UI
The problem I have is that when the ObservableCollection is added to, the Headers appear for the grouping on the DataGrid but the individual rows themselves don't.
Any help really appreciated,
Here is my XAML for the Datagrid
<DataGrid DataContext="{Binding GroupedBookings}"
ItemsSource="{Binding SourceCollection}"
AutoGenerateColumns="False"
SelectionMode="Single"
SelectionUnit="FullRow"
CanUserSortColumns="True"
SelectedItem="{Binding SelectedBooking}"
CanUserAddRows="False">
<DataGrid.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=MemberCount.SupporterType}"
FontWeight="Bold"
Padding="3" />
</StackPanel>
</DataTemplate>
</GroupStyle.HeaderTemplate>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<Expander>
<Expander.Header>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Name}" />
<TextBlock Text="{Binding Path=ItemCount}"
Margin="8 0 4 0" />
</StackPanel>
</Expander.Header>
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</DataGrid.GroupStyle>
<DataGrid.Columns>
<DataGridTextColumn Header="Cost"
Binding="{Binding Cost}" />
<DataGridTextColumn Header="Order No"
Binding="{Binding LinkedOrderID}" />
</DataGrid.Columns>
</DataGrid>
And my code for the Collections
_bookings = new ObservableCollection<Booking>(rep.Bookings_Get().Where(x => x.JobID == CurrentJob.JobID));
GroupedBookings = CollectionViewSource.GetDefaultView(Bookings);
GroupedBookings.GroupDescriptions.Add(new PropertyGroupDescription("MemberCount.SupporterType"));
To confirm the observable collection is updating fine as is the CollectionView in the VM, the ItemCount in the header even increases in the UI I just can't seem to make the rows appear.
Thanks in advance
Edit:
I have changed my code to assign directly to Bookings as opposed to _bookings as per EthicalLogics suggestion but this hasn't helped Bookings is defined as below:
public ObservableCollection<Booking> Bookings
{
get { return _bookings; }
set
{
_bookings = value;
OnPropertyChanged("Bookings");
}
}
Here is GroupedBookings
public ICollectionView GroupedBookings
{
get { return _groupedBookings; }
set
{
_groupedBookings = value;
OnPropertyChanged("GroupedBookings");
}
}
I added the following to my XAML, turns out I had missed something small however having looked at multiple examples of using a CollectionViewSource and grouping in a data grid I only found the microsoft one to contain this as part of the GroupStyle
<Expander.Content>
<ItemsPresenter />
</Expander.Content>
Hope this helps anyone that has a similar issue
public ObservableCollection<Booking> _bookings{get;set;}
Binding Source must be a property not field. Because binding system uses reflection and it looks only for properties not fields.I hope this will help.