I'm working on setting up a ListView whose Source property is set to an ivar of a class of mine, called Cat.
Each Cat has an ObservableCollection of Trait objects:
private ObservableCollection<Trait> _traits = new ObservableCollection<Trait>();
public ObservableCollection<Trait> Traits
{
get
{
return _traits;
}
}
public void AddTrait(Trait t)
{
_traits.Add(t);
// Is this redundant? Is one better than the other?
this.OnPropertyChanged("_traits");
this.OnPropertyChanged("Traits");
}
public IEnumerator<Object> GetEnumerator()
{
return _traits.GetEnumerator();
}
And then I'm assigning the Source property to this Traits collection:
this.CollectionViewSource.Source = CurrentCat.Traits;
This works properly, and the Trait objects are properly displayed in my ListView.
The issue is that changes to this underlying _traits collection do not cause the UI to update properly. For example, this:
void AddTraitButton_Click(object sender, RoutedEventArgs e)
{
if (this.CurrentCat != null)
{
this.CurrentCat.AddTrait(new Trait());
}
}
Doesn't seem to have any effect immediately in the UI, but if I reset the Source property like so:
var oldSource = this.CollectionViewSource.Source;
this.CollectionViewSource.Source = null;
this.CollectionViewSource.Source = oldSource;
Then the ListView updates properly. But, I'm sure there must be something that I'm missing, as I'd like for the UI to update upon the addition/removal of an item.
Edit: The CollectionViewSource is being applied to the ListView in my XAML file:
<CollectionViewSource x:Name="CollectionViewSource" x:Key="CollectionViewSource" />
...
<ListView x:Name="ItemListView" ItemsSource="{Binding Source={StaticResource CollectionViewSource}}" ...
I can't seem to find it now, but I seem to remember some problem with binding to CollectionViewSource. Have you tried binding directly to CurrentCat.Traits and setting this.DataContext = this in the code-behind (I am assuming you aren't using MVVM here)?
<ListView x:Name="ItemListView" ItemsSource="{Binding CurrentCat.Traits}" />
Rather than binding to the CollectionViewSource directly and replacing its Source to force a refresh, I believe you want to bind to the CVS's View property...
<ListView x:Name="ItemListView"
ItemsSource="{Binding Source={StaticResource CollectionViewSource}, Path=View}" ...
...and call CollectionViewSource.Refresh() after updating the source collection.
void AddTraitButton_Click(object sender, RoutedEventArgs e)
{
if (this.CurrentCat != null)
{
this.CurrentCat.AddTrait(new Trait());
this.CollectionViewSource.Refresh();
}
}
Also, a couple notes, since you seem relatively new to .NET/WPF conventions:
The private members of .NET classes are typically referred to as "fields" rather than "ivars" (Objective-C background? :))
Prefixing class members with the this keyword is usually redundant, unless there is another identifier in scope with the same name
It's worth exploring the MVVM and related patterns if you'll be doing anything non-trivial in WPF; they help you keep your views (XAML objects) as light and easy-to-change as possible.
In your case, for example, I assume the code you've shown is from the code-behind of whatever Window or UserControl contains your ListView. Following the MVVM pattern would involve creating a separate "ViewModel" class that would contain the Traits collection and expose it via a CollectionViewSource (using the View property, as I've mentioned). Your UserControl would then have an instance of the ViewModel assigned as its DataContext, and the ListView could be bound to the exposed CollectionView.
You may work exclusively with the ObservableCollection still. Although there is one problem - it would not show the data in IsInDesignMode. Maybe in the future it will improve.
public class MainViewModel : ViewModelBase
{
...
private ObservableCollection<PartViewModel> _parts;
public ObservableCollection<PartViewModel> Parts
{
get
{
if (_parts == null)
{
_parts = new ObservableCollection<PartViewModel>();
_parts.CollectionChanged += _parts_CollectionChanged;
}
return _parts;
}
}
object m_ReorderItem;
int m_ReorderIndexFrom;
void _parts_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Remove:
m_ReorderItem = e.OldItems[0];
m_ReorderIndexFrom = e.OldStartingIndex;
break;
case NotifyCollectionChangedAction.Add:
if (m_ReorderItem == null)
return;
var _ReorderIndexTo = e.NewStartingIndex;
m_ReorderItem = null;
break;
}
}
private PartViewModel _selectedItem;
public PartViewModel SelectedItem
{
get
{
return _selectedItem;
}
set
{
if (_selectedItem != value)
{
_selectedItem = value;
RaisePropertyChanged("SelectedItem");
}
}
}
...
#region ViewModelBase
public override void Cleanup()
{
if (_parts != null)
{
_parts.CollectionChanged -= _parts_CollectionChanged;
}
base.Cleanup();
}
#endregion
}
<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
<Grid.Resources>
<CollectionViewSource x:Name="PartsCollection" Source="{Binding Parts}"/>
</Grid.Resources>
<ListView Margin="20" CanReorderItems="True" CanDragItems="True" AllowDrop="True"
ItemsSource="{Binding Source={StaticResource PartsCollection}}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}" SelectionMode="Single">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"></Setter>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
...
</ListView.ItemTemplate>
</ListView>
</Grid>
Related
I have been trying to implement this for a while and haven't been able to do it so far, despite having the feeling that this should be something easy.
The difficulty comes from the fact that I have implemented a WPF application using the MVVM pattern. Now, this is my first attempt at both the pattern and the framework, so it is almost guaranteed that I have made mistakes while trying to follow the MVVM guidelines.
My implementation
I have three Views with their respective ViewModels (wired using Prism's AutoWireViewModel method). The MainView has a TabControl with two TabItems, each of witch contains a Frame container with the Source set to one of the other two Views. The following code is an excerpt of the MainView:
<TabControl Grid.Row="1" Grid.Column="1">
<TabItem Header="Test">
<!--TestView-->
<Frame Source="View1.xaml"/>
</TabItem>
<TabItem Header="Results">
<!--ResultsView-->
<Frame Source="View2.xaml"/>
</TabItem>
</TabControl>
My problem
Every time that someone changes to a specific TabItem, I would like to run a method that updates one of the WPF controls included in that View. The method is already implemented and bound to a Button, but ideally, no button should be necessary, I would like to have some kind of Event to make this happen.
I appreciate all the help in advance.
You could for example handle the Loaded event of the Page to either call a method or invoke a command of the view model once the view has been loaded initially:
public partial class View2 : Page
{
public View2()
{
InitializeComponent();
Loaded += View2_Loaded;
}
private void View2_Loaded(object sender, RoutedEventArgs e)
{
var viewModel = DataContext as ViewModel2;
if (viewModel != null)
viewModel.YourCommand.Execute(null);
Loaded -= View2_Loaded;
}
}
The other option would be handle this in the MainViewModel. You bind the SelectedItem property of the TabControl to a property of the MainViewModel and set this property to an instance of either ViewModel2 or ViewModel2, depending on what kind of view you want to display.
You could then call any method or invoked any command you want on these. But this is another story and then you shouldn't hardcode the TabItems in the view and use Frame elements to display Pages. Please take a look here for an example:
Selecting TabItem in TabControl from ViewModel
Okay, so What I have done is Create a Custom Tab Control. I will write out step by step instructions for this, and then you can add edit to it.
Right click on your solution select add new project
Search For Custom Control Library
High Light the name of the class that comes up, and right click rename it to what ever you want I named it MyTabControl.
Add Prism.Wpf to the new project
Add a reference to the new project to where ever your going to need it. I needed to add to just the main application, but if you have a separate project that only has views then you will need to add it to that too.
Inherit your Custom Control From TabControl Like:
public class MyTabControl : TabControl
You will notice that there is a Themes folder in the project you will need to open the Generic.xaml and edit it. it should look like:
TargetType="{x:Type local:MyTabControl}" BasedOn="{StaticResource {x:Type TabControl}}" for some reason this will not let me show the style tags but they will need to be in there as well
Please review this code I got this from Add A Command To Custom Control
public class MyTabControl : TabControl
{
static MyTabControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyTabControl), new FrameworkPropertyMetadata(typeof(MyTabControl)));
}
public static readonly DependencyProperty TabChangedCommandProperty = DependencyProperty.Register(
"TabChangedCommand", typeof(ICommand), typeof(MyTabControl),
new PropertyMetadata((ICommand)null,
new PropertyChangedCallback(CommandCallBack)));
private static void CommandCallBack(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var myTabControl = (MyTabControl)d;
myTabControl.HookupCommands((ICommand) e.OldValue, (ICommand) e.NewValue);
}
private void HookupCommands(ICommand oldValue, ICommand newValue)
{
if (oldValue != null)
{
RemoveCommand(oldValue, oldValue);
}
AddCommand(oldValue, oldValue);
}
private void AddCommand(ICommand oldValue, ICommand newCommand)
{
EventHandler handler = new EventHandler(CanExecuteChanged);
var canExecuteChangedHandler = handler;
if (newCommand != null)
{
newCommand.CanExecuteChanged += canExecuteChangedHandler;
}
}
private void CanExecuteChanged(object sender, EventArgs e)
{
if (this.TabChangedCommand != null)
{
if (TabChangedCommand.CanExecute(null))
{
this.IsEnabled = true;
}
else
{
this.IsEnabled = false;
}
}
}
private void RemoveCommand(ICommand oldCommand, ICommand newCommand)
{
EventHandler handler = CanExecuteChanged;
oldCommand.CanExecuteChanged -= handler;
}
public ICommand TabChangedCommand
{
get { return (ICommand) GetValue(TabChangedCommandProperty); }
set { SetValue(TabChangedCommandProperty, value); }
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.SelectionChanged += OnSelectionChanged;
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (TabChangedCommand != null)
{
TabChangedCommand.Execute(null);
}
}
}
you will need to add the name space in your window or usercontrol like:
xmlns:wpfCustomControlLibrary1="clr-namespace:WpfCustomControlLibrary1;assembly=WpfCustomControlLibrary1"
and here is your control:
<wpfCustomControlLibrary1:MyTabControl TabChangedCommand="{Binding TabChangedCommand}">
<TabItem Header="View A"></TabItem>
<TabItem Header="View B"></TabItem>
</wpfCustomControlLibrary1:MyTabControl>
This is how I'd approach this sort of requirement:
View:
<Window.DataContext>
<local:MainWIndowViewModel/>
</Window.DataContext>
<Grid>
<TabControl Name="tc" ItemsSource="{Binding vms}">
<TabControl.Resources>
<DataTemplate DataType="{x:Type local:uc1vm}">
<local:UserControl1/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:uc2vm}">
<local:UserControl2/>
</DataTemplate>
</TabControl.Resources>
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="Header" Value="{Binding TabHeading}"/>
</Style>
</TabControl.ItemContainerStyle>
</TabControl>
</Grid>
</Window>
When it has a uc1vm it will be templated into usercontrol1 in the view.
I'm binding to a collection of viewmodels which all implement an interface so I know for sure I can cast to that and call a method.
Main viewmodel for window:
private IDoSomething selectedVM;
public IDoSomething SelectedVM
{
get { return selectedVM; }
set
{
selectedVM = value;
selectedVM.doit();
RaisePropertyChanged();
}
}
public ObservableCollection<IDoSomething> vms { get; set; } = new ObservableCollection<IDoSomething>
{ new uc1vm(),
new uc2vm()
};
public MainWIndowViewModel()
{
}
When a tab is selected, the setter for selected item will be passed the new value. Cast that and call the method.
My interface is very simple, since this is just illustrative:
public interface IDoSomething
{
void doit();
}
An example viewmodel, which is again just illustrative and doesn't do much:
public class uc1vm : IDoSomething
{
public string TabHeading { get; set; } = "Uc1";
public void doit()
{
// Your code goes here
}
}
I appreciate all of your input, but I found an alternative solution. Given the information given by #mm8, I took advantage of the Loaded event but in a way that does not require any code in the code behind.
My solution
In the View which I would like to give this ability to execute a method every time the user selects the TabItem that contains it, I added the following code:
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding OnLoadedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
And then simply implemented a DelegateCommand called OnLoadedCommand in the View's respective ViewModel. Inside that command I call my desired method.
Please comment if you spot anything wrong with this approach! I chose to try this since it required the least amount of changes to my code, but I may be missing some vital information regarding problems the solution may cause.
I used Drag and Drop to bind Data Source object (a DB model) to DataGrid (basically following this example in Entity Framework Databinding with WPF.
Everything works fine with this implementation.
XAML
<Window.Resources>
<CollectionViewSource x:Key="categoryViewSource"
d:DesignSource="{d:DesignInstance {x:Type local:Category}, CreateList=True}"/>
</Window.Resources>
<Grid DataContext="{StaticResource categoryViewSource}">
..
Code Behind
private void Window_Loaded(object sender, RoutedEventArgs e)
{
System.Windows.Data.CollectionViewSource categoryViewSource =
((System.Windows.Data.CollectionViewSource)(this.FindResource("categoryViewSource")));
_context.Categories.Load();
categoryViewSource.Source = _context.Categories.Local;
}
ViewModel
public MainWindow()
{
InitializeComponent();
this.DataContext = new MyViewModel();
}
However, when I try to use the same code from within ViewModel, it doesn‘t work (FindResource is not available), besides, I don’t think this is the right approach (i.e. to use x:Key in MVVM).
I would really appreciate any help to point me what is the right way to implement CollectionViewSource and DataBinding with DataGrid.
You have two options to use CollectionViewSource properly with MVVM -
Expose an ObservableCollection of items (Categories in your case) through your ViewModel and create CollectionViewSource in XAML like this -
<CollectionViewSource Source="{Binding Path=Categories}">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="CategoryName" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
scm: xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
see this - Filtering collections from XAML using CollectionViewSource
Create and Expose an ICollectionView directly from your ViewModel
see this - How to Navigate, Group, Sort and Filter Data in WPF
Following example shows how to create a collection view and
bind it to a ListBox
View XAML:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
x:Class="CustomerView">
<ListBox ItemsSource={Binding Customers} />
</Window>
View Codebehind:
public class CustomerView : Window
{
public CustomerView()
{
DataContext = new CustomerViewModel();
}
}
ViewModel:
public class CustomerViewModel
{
private readonly ICollectionView customerView;
public ICollectionView Customers
{
get { return customerView; }
}
public CustomerViewModel()
{
IList<Customer> customers = GetCustomers();
customerView = CollectionViewSource.GetDefaultView( customers );
}
}
Update:
Q. If there is no property to sort on? e.g. if there is an ObservableCollection of string or int?
A. In that case you can Simply use . as the property name:
<scm:SortDescription PropertyName="." />
I found that it is handy to have a CollectionViewSource in my ViewModel and bind the ListBox (in my case) to the CollectionViewSource.View while setting the CollectionViewSource.Source to be the list I want to use.
Like so:
ViewModel:
public DesignTimeVM() //I'm using this as a Design Time VM
{
Items = new List<Foo>();
Items.Add(new Foo() { FooProp= "1", FooPrep= 20.0 });
Items.Add(new Foo() { FooProp= "2", FooPrep= 30.0 });
FooViewSource = new CollectionViewSource();
FooViewSource.Source = Items;
SelectedFoo = Items.First();
//More code as needed
}
XAML:
<ListBox ItemsSource="{Binding FooViewSource.View}" SelectedItem="{Binding SelectedFoo}"/>
This means I can do neat stuff in the VM as needed (from https://blogs.msdn.microsoft.com/matt/2008/08/28/collectionview-deferrefresh-my-new-best-friend/ ):
using (FooViewSource.DeferRefresh())
{
//Remove an old Item
//add New Item
//sort list anew, etc.
}
I suppose this is possible when using the ICollectionView object also, but the demo code in the blog link seems to be some codebehind stuff, refering the listbox directly, which I'm trying to avoid.
BTW before you ask, here's how you use a Design Time VM: WPF Design Time View Model
Just for reference, another way is to use an attached property on the CollectionViewSource which then pipes the functions to the ViewModel (Implementing an Interface).
This is a very basic Demonstration just for filtering, it would need some work for e.g. a second Collection on the VM but i think it's enough to show the general technique.
If this is better or worse than the other methods is up for discussion, i just wanted to point out, that there's another way of doing this
Definition of attached Property:
public static class CollectionViewSourceFilter
{
public static IFilterCollectionViewSource GetFilterObject(CollectionViewSource obj)
{
return (IFilterCollectionViewSource)obj.GetValue(FilterObjectProperty);
}
public static void SetFilterObject(CollectionViewSource obj, IFilterCollectionViewSource value)
{
obj.SetValue(FilterObjectProperty, value);
}
public static void FilterObjectChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (e.OldValue is IFilterCollectionViewSource oldFilterObject
&& sender is CollectionViewSource oldCvs)
{
oldCvs.Filter -= oldFilterObject.Filter;
oldFilterObject.FilterRefresh -= (s, e2) => oldCvs.View.Refresh();
}
if (e.NewValue is IFilterCollectionViewSource filterObject
&& sender is CollectionViewSource cvs)
{
cvs.Filter += filterObject.Filter;
filterObject.FilterRefresh += (s,e2) => cvs.View.Refresh();
}
}
public static readonly DependencyProperty FilterObjectProperty = DependencyProperty.RegisterAttached(
"FilterObject",
typeof(Interfaces.IFilterCollectionViewSource),
typeof(CollectionViewSourceFilter),
new PropertyMetadata(null,FilterObjectChanged)
);
}
Interface:
public interface IFilterCollectionViewSource
{
void Filter(object sender, FilterEventArgs e);
event EventHandler FilterRefresh;
}
usage in xaml:
<CollectionViewSource
x:Key="yourKey"
Source="{Binding YourCollection}"
classes:CollectionViewSourceFilter.FilterObject="{Binding}" />
and usage in the ViewModel:
class YourViewModel : IFilterCollectionViewSource
{
public event EventHandler FilterRefresh;
private string _SearchTerm = string.Empty;
public string SearchTerm
{
get { return _SearchTerm; }
set {
SetProperty(ref _SearchTerm, value);
FilterRefresh?.Invoke(this, null);
}
}
private ObservableCollection<YourItemType> _YourCollection = new ObservableCollection<YourItemType>();
public ObservableCollection<YourItemType> YourCollection
{
get { return _YourCollection; }
set { SetProperty(ref _YourCollection, value); }
}
public void Filter(object sender, FilterEventArgs e)
{
e.Accepted = (e.Item as YourItemType)?.YourProperty?.ToLower().Contains(SearchTerm.ToLower()) ?? true;
}
}
I am making a WPF application that is navigable via custom "Next" and "Back" buttons and commands (i.e. not using a NavigationWindow). On one screen, I have a ListBox that has to support multiple selections (using the Extended mode). I have a view model for this screen and store the selected items as a property, since they need to be maintained.
However, I am aware that the SelectedItems property of a ListBox is read-only. I have been trying to work around the issue using this solution here, but I have not been able to adopt it into my implementation. I found that I can't differentiate between when one or more elements are deselected and when I navigate between screens (NotifyCollectionChangedAction.Remove is raised in both cases, since technically all the selected items are deselected when navigating away from the screen). My navigation commands are located in a separate view model which manages the view models for each screen, so I can't put any implementation related to the view model with the ListBox in there.
I have found several other less elegant solutions, but none of these seem to enforce a two-way binding between the view model and the view.
Any help would be greatly appreciated. I can provide some of my source code if it would help to understand my problem.
Try creating an IsSelected property on each of your data items and binding ListBoxItem.IsSelected to that property
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
Rachel's solutions works great! But there is one problem I've encountered - if you override the style of ListBoxItem, you loose the original styling applied to it (in my case responsible for highlighting the selected item etc.). You can avoid this by inheriting from the original style:
<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
Note setting BasedOn (see this answer)
.
I couldn't get Rachel's solution to work how I wanted it, but I found Sandesh's answer of creating a custom dependency property to work perfectly for me. I just had to write similar code for a ListBox:
public class ListBoxCustom : ListBox
{
public ListBoxCustom()
{
SelectionChanged += ListBoxCustom_SelectionChanged;
}
void ListBoxCustom_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
SelectedItemsList = SelectedItems;
}
public IList SelectedItemsList
{
get { return (IList)GetValue(SelectedItemsListProperty); }
set { SetValue(SelectedItemsListProperty, value); }
}
public static readonly DependencyProperty SelectedItemsListProperty =
DependencyProperty.Register(nameof(SelectedItemsList), typeof(IList), typeof(ListBoxCustom), new PropertyMetadata(null));
}
In my View Model I just referenced that property to get my selected list.
I kept looking into an easy solution for this but with no luck.
The solution Rachel has is good if you already have the Selected property on the object within your ItemsSource. If you do not, you have to create a Model for that business model.
I went a different route. A quick one, but not perfect.
On your ListBox create an event for SelectionChanged.
<ListBox ItemsSource="{Binding SomeItemsSource}"
SelectionMode="Multiple"
SelectionChanged="lstBox_OnSelectionChanged" />
Now implement the event on the code behind of your XAML page.
private void lstBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var listSelectedItems = ((ListBox) sender).SelectedItems;
ViewModel.YourListThatNeedsBinding = listSelectedItems.Cast<ObjectType>().ToList();
}
Tada. Done.
This was done with the help of converting SelectedItemCollection to a List.
Here's yet another solution. It's similar to Ben's answer, but the binding works two ways. The trick is to update the ListBox's selected items when the bound data items change.
public class MultipleSelectionListBox : ListBox
{
public static readonly DependencyProperty BindableSelectedItemsProperty =
DependencyProperty.Register("BindableSelectedItems",
typeof(IEnumerable<string>), typeof(MultipleSelectionListBox),
new FrameworkPropertyMetadata(default(IEnumerable<string>),
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged));
public IEnumerable<string> BindableSelectedItems
{
get => (IEnumerable<string>)GetValue(BindableSelectedItemsProperty);
set => SetValue(BindableSelectedItemsProperty, value);
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
BindableSelectedItems = SelectedItems.Cast<string>();
}
private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is MultipleSelectionListBox listBox)
listBox.SetSelectedItems(listBox.BindableSelectedItems);
}
}
Unfortunately, I wasn't able to use IList as the BindableSelectedItems type. Doing so sent null to my view model's property, whose type is IEnumerable<string>.
Here's the XAML:
<v:MultipleSelectionListBox
ItemsSource="{Binding AllMyItems}"
BindableSelectedItems="{Binding MySelectedItems}"
SelectionMode="Multiple"
/>
There's one thing to watch out for. In my case, a ListBox may be removed from the view. For some reason, this causes the SelectedItems property to change to an empty list. This, in turn, causes the view model's property to be changed to an empty list. Depending on your use case, this may not be desirable.
This was pretty easy to do with a Command and the Interactivities EventTrigger. ItemsCount is just a bound property to use on your XAML, should you want to display the updated count.
XAML:
<ListBox ItemsSource="{Binding SomeItemsSource}"
SelectionMode="Multiple">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding SelectionChangedCommand}"
CommandParameter="{Binding ElementName=MyView, Path=SelectedItems.Count}" />
</i:EventTrigger>
</Interaction.Triggers>
</ListView>
<Label Content="{Binding ItemsCount}" />
ViewModel:
private int _itemsCount;
private RelayCommand<int> _selectionChangedCommand;
public ICommand SelectionChangedCommand
{
get {
return _selectionChangedCommand ?? (_selectionChangedCommand =
new RelayCommand<int>((itemsCount) => { ItemsCount = itemsCount; }));
}
}
public int ItemsCount
{
get { return _itemsCount; }
set {
_itemsCount = value;
OnPropertyChanged("ItemsCount");
}
}
Turns out binding a check box to the IsSelected property and putting the textblock and checkbox within a stack panel does the trick!
Not satisfied with the given answers I was trying to find one by myself...
Well it turns out to be more like a hack then a solution but for me that works fine. This Solution uses MultiBindings in a special way.
First it may look like a ton of Code but you can reuse it with very little effort.
First I implemented a 'IMultiValueConverter'
public class SelectedItemsMerger : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
SelectedItemsContainer sic = values[1] as SelectedItemsContainer;
if (sic != null)
sic.SelectedItems = values[0];
return values[0];
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
return new[] { value };
}
}
And a SelectedItems Container/Wrapper:
public class SelectedItemsContainer
{
/// Nothing special here...
public object SelectedItems { get; set; }
}
Now we create the Binding for our ListBox.SelectedItem (Singular). Note: You have to create a static Resource for the 'Converter'. This may be done once per application and be reused for all ListBoxes that need the converter.
<ListBox.SelectedItem>
<MultiBinding Converter="{StaticResource SelectedItemsMerger}">
<Binding Mode="OneWay" RelativeSource="{RelativeSource Self}" Path="SelectedItems"/>
<Binding Path="SelectionContainer"/>
</MultiBinding>
</ListBox.SelectedItem>
In the ViewModel I created the Container where I can bind to. It is important to initialize it with new() in order to fill it with the values.
SelectedItemsContainer selectionContainer = new SelectedItemsContainer();
public SelectedItemsContainer SelectionContainer
{
get { return this.selectionContainer; }
set
{
if (this.selectionContainer != value)
{
this.selectionContainer = value;
this.OnPropertyChanged("SelectionContainer");
}
}
}
And that's it. Maybe someone sees some improvements?
What do You think about it?
This was a major issue for me, some of the answers I have seen were either too hackish, or required resetting the SelectedItems property value breaking any code attached to the properties OnCollectionChanged event. But I managed to get a workable solution by modifying the collection directly and as a bonus it even supports SelectedValuePath for object collections.
public class MultipleSelectionListBox : ListBox
{
internal bool processSelectionChanges = false;
public static readonly DependencyProperty BindableSelectedItemsProperty =
DependencyProperty.Register("BindableSelectedItems",
typeof(object), typeof(MultipleSelectionListBox),
new FrameworkPropertyMetadata(default(ICollection<object>),
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged));
public dynamic BindableSelectedItems
{
get => GetValue(BindableSelectedItemsProperty);
set => SetValue(BindableSelectedItemsProperty, value);
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
if (BindableSelectedItems == null || !this.IsInitialized) return; //Handle pre initilized calls
if (e.AddedItems.Count > 0)
if (!string.IsNullOrWhiteSpace(SelectedValuePath))
{
foreach (var item in e.AddedItems)
if (!BindableSelectedItems.Contains((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null)))
BindableSelectedItems.Add((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null));
}
else
{
foreach (var item in e.AddedItems)
if (!BindableSelectedItems.Contains((dynamic)item))
BindableSelectedItems.Add((dynamic)item);
}
if (e.RemovedItems.Count > 0)
if (!string.IsNullOrWhiteSpace(SelectedValuePath))
{
foreach (var item in e.RemovedItems)
if (BindableSelectedItems.Contains((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null)))
BindableSelectedItems.Remove((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null));
}
else
{
foreach (var item in e.RemovedItems)
if (BindableSelectedItems.Contains((dynamic)item))
BindableSelectedItems.Remove((dynamic)item);
}
}
private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is MultipleSelectionListBox listBox)
{
List<dynamic> newSelection = new List<dynamic>();
if (!string.IsNullOrWhiteSpace(listBox.SelectedValuePath))
foreach (var item in listBox.BindableSelectedItems)
{
foreach (var lbItem in listBox.Items)
{
var lbItemValue = lbItem.GetType().GetProperty(listBox.SelectedValuePath).GetValue(lbItem, null);
if ((dynamic)lbItemValue == (dynamic)item)
newSelection.Add(lbItem);
}
}
else
newSelection = listBox.BindableSelectedItems as List<dynamic>;
listBox.SetSelectedItems(newSelection);
}
}
}
Binding works just as you would have expected MS to have done themselves:
<uc:MultipleSelectionListBox
ItemsSource="{Binding Items}"
SelectionMode="Extended"
SelectedValuePath="id"
BindableSelectedItems="{Binding mySelection}"
/>
It has not been thoroughly tested but has passed first glance inspections. I tried to keep it reuseable by employing dynamic types on the collections.
It took me a while to implement binding/using SelectedItems as I am not an expert at this so I wanted to share my solution if someone might find it useful. Do not forget to download Microsoft.Xaml.Behaviors.Wpf from Nuget for this solution.
I have benefited from Accessing WPF ListBox SelectedItems
View:
Window x:Class="WpfAppSelectedItems.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:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:local="clr-namespace:WpfAppSelectedItems"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<ListBox Height="250" Width="300"
ItemsSource="{Binding Items}" SelectionMode="Extended"
>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.InputBindings>
<KeyBinding Gesture="Ctrl+A" Command="{Binding SelectAllCommand}" />
</ListBox.InputBindings>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged" >
<i:CallMethodAction TargetObject="{Binding}" MethodName="ListBox_SelectionChanged"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ListBox>
</Grid>
</Window>
`
Code behind:
namespace WpfAppSelectedItems
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new ViewModel(); //connecting window to VM
}
}
}
ViewModel:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using Microsoft.Xaml.Behaviors;
using System.Windows;
namespace WpfAppSelectedItems
{
internal class ViewModel: Presenter
{
//Creating ItemPresenter class. IsSelected binded to Style in the view
public class ItemPresenter : Presenter
{
private readonly string _value;
public ItemPresenter(string value)
{
_value = value;
}
public override string ToString()
{
return _value;
}
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
OnPropertyChanged();
}
}
}
//Placing items to the Items which is binded to the ListBox
public ObservableCollection<ItemPresenter> Items { get; } = new ObservableCollection<ItemPresenter>
{
new ItemPresenter("A"),
new ItemPresenter("B"),
new ItemPresenter("C"),
new ItemPresenter("D")
};
//Do something when selection changed including detecting SelectedItems
public void ListBox_SelectionChanged()
{
foreach (var item in Items)
{
if (item.IsSelected)
MessageBox.Show(fufuitem.ToString());
}
}
};
//Notify View if a property changes
public abstract class Presenter : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
if you just want to get the Name of the selected Element you can do the following:
View:
<ListBox
x:Name="Folders"
Grid.Row="1"
Grid.Column="0"
ItemsSource="{Binding YourListWithStings}"
SelectionMode="Single"
SelectedItem="{Binding ToYourOutputVariable}"
>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Viewmodel:
private string _ToYourOutputVariable
public string ToYourOutputVariable
{
get {return _ToYourOutputVariable; }
set
{
_ToYourOutputVariable = value;
NotifyOfPropertyChange();
MessageBox.Show(_ToYourOutputVariable);
}
}
The messageBox shows the name of the selected listitem. You could call a function where you open the MessageBox
I have ObservableCollection items i want to bind this data to my comboBox.
How to bind data with comboBox?
I am using MVVM pattern so suggest me how to bind data using MVVM pattern
I am trying to do this code but not working properly..
In my XAML PAGE:
<ComboBox x:Name="comobo1"
DisplayMemberPath="CardTypeName"
SelectedValuePath="CardTypeID"
ItemsSource="{Binding Path=combodata}">
</ComboBox>
In my ViewModel
(Card is my model)
public ObservableCollection<Card> combodata = new ObservableCollection<Card>();
foreach (var item in App.db.States)
{
Card c = new Card(item.StateName, item.StateID);
combodata.Add(c);
}
How to bind this combodata to my comboBox - what am I doing wrong ?
At first: your combodata has private access modifier instead of public. At second: combodata must be property but not the field. And you'd better add INotifyPropertyChanged implementation to you class.
You have to bind to a public property of the view model that should implement INotifyPropertyChanged.
Here's what you should do:
View:
<ComboBox x:Name="comobo1" DisplayMemberPath="CardTypeName" SelectedValuePath="CardTypeID" ItemsSource="{Binding Path=ComboData}" />
ViewModel:
public class MyViewModel : INotifyPropertyChanged
{
private ObservableCollection<Card> comboData;
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<Card> ComboData
{
get
{
return this.comboData;
}
set
{
if (this.comboData != value)
{
this.comboData = value;
this.NotifyPropertyChanged("ComboData");
}
}
}
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
EDIT: you'll also need to set the DataContext property of your view. The simple way is to affect the instance of your ViewModel to the DataContext property in the code-behind of your view.
private ObservableCollection<Card> _combodata;
Public ObservableCollection<Card> comboData
{
get
{
if (_combodata == null)
_combodata = new ObservableCollection<Card>();
return _combodata;
}
set
{
if (value != _combodata)
_combodata = value;
}
}
<ComboBox x:Name="comobo1"
DisplayMemberPath="CardTypeName"
SelectedValuePath="CardTypeID"
ItemsSource="{Binding Path=comboData}">
</ComboBox>
And don't forget to set a DataContext property.
There are following problems with the code.
1.You can not bind to private field or property. It should be public property.
2.You only provided ItemsSource="{Binding Path=combodata}" but did not provide a source. Where does combodata come from ?
ObservableCollection has nothing to do unless your combo box items doe not change once filled. In this case List can work well.
To make it work, Change your combodata to public property as
public ObservableCollection<Card> combodata {get;set;}
then,
<ComboBox x:Name="comobo1"
DisplayMemberPath="CardTypeName"
SelectedValuePath="CardTypeID"
ItemsSource="{Binding Path=combodata}" ElementName=mainWindow>
</ComboBox>
by specifying ElementName you are telling WPF binding engine to look for the combodata property of the mainWindow class.
I hope this helps.
My best guess is your ComboBox's DataContext is not set to an instance of your ViewModel
I often use Snoop to debug DataContext problems with the application. It allows you to view your Visual Tree, and see what the DataContext is for all controls.
The DataContext is the data your UI is bound to. Usually the DataContext is set higher up in the Visual Tree, such as on the Window object, although as an example the following line of code would set the ComboBox's DataContext to a new instance of your ViewModel, and then your ComboBox should be able to find the combodata collection to bind to it.
comobo1.DataContext = new MyViewModel();
Also, change your combodata from a Field (no get/set accessor methods) to a Property (see Dmitriy's Answer for an example)
You need to bind to a public property. In your example, combodata is private.
Correct Answer is :
In XAML PAGE":
<CollectionViewSource x:Key="comboBoxCollection" Source="{Binding comboData}"></CollectionViewSource>
<DataTemplate x:Key="ComboBoxDataTemplate">
<Grid MinHeight="25">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ComboBox x:Name="comobo1" DisplayMemberPath="CardTypeName" SelectedValuePath="CardTypeID" ItemsSource="{Binding Source={StaticResource comboBoxCollection }}">
</ComboBox>
</Grid>
</DataTemplate>
In ViewModel:
private ObservableCollection<Card> _combodata;
public ObservableCollection<Card> comboData
{
get
{
if (_combodata == null)
_combodata = new ObservableCollection<Card>();
return _combodata;
}
set
{
if (value != _combodata)
_combodata = value;
}
}
if (_objCardField.FieldTag == "State")
{
cards = new Cards();
foreach (var item in App.db.States)
{
Card c = new Card(item.StateName, item.StateID);
comboData.Add(c);
}
}
I have made a tree View in wpf Using MVVM .
it is working fine but here is one problem that leaf node contains some checkboxes and user have only two options either to select one or none .
So here how i can restricted user to select maximum only one cold drink.
I did one trick but it didn't work that when i have already selected a drink and then i select another one than i set the last selected value in the observable collection to false but it doesn't affect on view and selected check boxes remains selected although in collection only one option's value is true.
I cant use radio button instedof checkbox becasue user can select none of the options and i cant give an additional option for none of the above.
If any one have any solution so please let me know I'll be very thankful.
updated question:
i think i didn't define my problem in a proper way so i am giving my code snipperts here hope by this i'll get the solution o f my problem...
My View Model Class
namespace TestViewModels
{
public class ViewModel :ViewModelBase
{
private ObservableCollection<AvailableProducts> _MyTreeViewProperty
public ObservableCollection<AvailableProducts> MyTreeViewProperty
{
get { return _MyTreeViewProperty
set { _MyTreeViewProperty value;
RaisePropertyChanged("MyTreeViewProperty");}
}
}
public class AvailableProducts
{
private string _BrandName;
public string BrandName
{
get { return _BrandName
set { _BrandName = value; }
}
private bool _IsExpanded;
public bool IsExpanded
{
get
{
return _IsExpanded;
}
set
{
_IsExpanded = value;
}
}
private ObservableCollection<ProductTypes> _MyProductTypes
public ObservableCollection<ProductTypes> MyProductTypes
{
get { return _MyProductTypes}
set { _MyProductTypes= value; }
}
}
public class ProductTypes
{
private string _ProductTypeName;
public string ProductTypeName
{
get { return _ProductTypeName;
set { _ProductTypeNamevalue; }
}
private ObservableCollection<ProductSubTypes> _ProdSubTypes;
public ObservableCollection<ProductSubTypes> ProdSubTypes
{
get { return _ProdSubTypes;}
set { _ProdSubTypes;= value; }
}
}
public class ProductSubTypes
{
private string _ProductSubTypeName;
public string ProductSubTypeName
{
get { return _ProductSubTypeName;
set { _ProductSubTypeName;}
}
private int _ParentID;
public int ParentID
{
get { return _ParentID;}
set { _ParentID;= value; }
}
private bool _IsAssigned;
public bool IsAssigned
{
get { return _IsAssigned; }
set
{
_IsAssigned = value;
if _ParentID;!= 0)
{
//updating data in database
//Calling and setting new collection value in property
//issue : updated collection sets in setter of MyTreeViewProperty but before calling getter
// it comes to IsAssigned getter so view doesnt get updated collection of MyTreeViewProperty
}
RaisePropertyChanged("IsAssigned");
}
}
}
}
View
<Page x:Class="ShiftManagerViews.Pages.ProductTreeSelection
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
DataContext="{Binding ProductsTree, Source={StaticResource Locator}}"
mc:Ignorable="d" Width="870" Height="665"
>
<TreeView Margin="10,10,0,13" ItemsSource="{Binding MyTreeViewProperty, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left"
VerticalAlignment="Top" Width="800" Height="Auto" MinHeight="400" MaxHeight="800">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:AvailableProducts}"
ItemsSource="{Binding MyProductTypes}">
<WrapPanel>
<Image Width="20" Height="20" Source="/ShiftManagerViews;component/Images/12.bmp"/>
<Label Content="{Binding BrandName}" FontSize="14"/>
</WrapPanel>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:ProductTypes}"
ItemsSource="{Binding ProdSubTypes}">
<WrapPanel>
<Image Width="18" Height="15" Source="/ShiftManagerViews;component/Images/12.bmp"/>
<Label Content="{Binding ProductTypeName}" FontSize="13"/>
</WrapPanel>
</HierarchicalDataTemplate>
<!-- the template for showing the Leaf node's properties-->
<DataTemplate DataType="{x:Type local:ProductSubTypes}">
<StackPanel>
<CheckBox IsChecked="{Binding IsAssigned, Mode=TwoWay}" Content="{Binding ProductSubTypeName}" Height="25">
</CheckBox>
</StackPanel>
</DataTemplate>
</TreeView.Resources>
</TreeView>
What about using a ListBox to display sub-items instead of a TreeView? You can style that so the items contain a CheckBox to show IsSelected instead of highlighting the item.
I'd suggest your user interface is wrong. If the user can only pick one then it would be better to swap these for radio buttons and add a "None of the above" option. That'll then give you the behaviour you want for free and your UI will be more intuitive.
EDIT: Since you say you can't add a "None" option and want to use a checkbox (even though I strongly disagree on checkboxes where a radio button is more appropriate - a common UI error)...
The technical problem you are probably facing is that an ObservableCollection only raises notification events if the collection itself changes. i.e. Only if items are added or removed. It does not raised events when items within the collection change, therefore the changing the status of the checkbox in the code will not raise the event for the UI binding to act on.
One solution to this to write a custom class that extends ObservableCollection that does provide this behaviour
From MSDN:
If you need to know if someone has changed a property of one of the
items within the collection, you'll need to ensure that the items in
the collection implement the INotifyPropertyChanged interface, and
you'll need to manually attach property changed event handlers for
those objects. No matter how you change properties of objects within
the collection, the collection's PropertyChanged event will not fire.
As a matter of fact, the ObservableCollection's PropertyChanged event
handler is protected—you can't even react to it unless you inherit
from the class and expose it yourself. You could, of course, handle
the PropertyChanged event for each item within the collection from
your inherited collection
I upvoted Rachel's answer, it is a common way in WPF to databind sets of radio buttons or check boxes. If you still want to go the tree view way, below code works. All view related code is in the view, so below code follows MVVM principles. If you are a MVVM purist you can put the code behind and a TreeView control in a user control if you do not want any code behind.
XAML:
<TreeView ItemsSource="{Binding Path=Drinks}">
<TreeView.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding .}" Checked="OnCheckBoxChecked" Unchecked="OnCheckBoxUnchecked" Loaded="OnCheckBoxLoaded" />
</DataTemplate>
</TreeView.ItemTemplate>
</TreeView>
Code behind + VM:
public partial class Window1
{
public Window1()
{
InitializeComponent();
DataContext = new VM();
}
private void OnCheckBoxChecked(object sender, System.Windows.RoutedEventArgs e)
{
foreach (CheckBox checkBox in _checkBoxes.Where(cb => cb != sender))
{
checkBox.IsChecked = false;
}
(DataContext as VM).CurrentDrink = (sender as CheckBox).Content.ToString();
}
private void OnCheckBoxUnchecked(object sender, System.Windows.RoutedEventArgs e)
{
(DataContext as VM).CurrentDrink = null;
}
private void OnCheckBoxLoaded(object sender, System.Windows.RoutedEventArgs e)
{
_checkBoxes.Add(sender as CheckBox);
}
private List<CheckBox> _checkBoxes = new List<CheckBox>();
}
public class VM
{
public List<string> Drinks
{
get
{
return new List<string>() { "Coffee", "Tea", "Juice" };
}
}
public string CurrentDrink { get; set; }
}
I did one trick but it didn't work that when i have already selected a
drink and then i select another one than i set the last selected value
in the observable collection to false but it doesn't affect on view
and selected check boxes remains selected although in collection only
one option's value is true.
Make sure that your child objects (AvailableProducts
and SubProductTypes) also implement INotifyPropertyChanged, this will make sure that the UI receives changes when modify the object.
Once all of you objects update the UI properly you will be able to layer in, and test, whatever custom business logic you need.
So if you have a product type that can only have one sub chosen, you could add a property on ProductType called OnlyAllowOneChild. Whenever, a child object raises a IsAssigned changed event, the parent can set false all other children. This of course requires you to have the parent either register for the children's PropertyChangedEvent, or got grab an EventAggregator (MVVMLight Messenger, or PRISM EvenAggregator) and create a messaging system.
Finally i am succeeded to solve my problem.
on Is Assigned property i am updating my database values and calling a method in view using MVVM Light messaging and passing currently selected leaf's parent id in it as a parameter...
Added a property in class Product Types to expand the parent node of the last selected leaf..
In view's method i am refreshing data context's source and passing currently selected leaf's parent id tO the VM to set its Is Expanded property value to true...
By this my view is working perfectly as same as i want...
If any body have solution better than this than I'll be happy to know.