Binding SelectedItems of ListView to ViewModel - c#

I have a list view that binding items with a property in viewmodel.
<ListView Height="238"
HorizontalAlignment="Left"
Name="listView"
VerticalAlignment="Top"
Width="503"
ItemsSource="{Binding BusinessCollection}"
SelectionMode="Multiple">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding RelativeSource={RelativeSource AncestorType={x:Type ListViewItem}}, Path=IsSelected}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn DisplayMemberBinding="{Binding ID}" Header="ID" />
<GridViewColumn DisplayMemberBinding="{Binding Name}" Header="Name" />
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
and in viewmodel.
ICollectionView _businessCollection
public ICollectionView BusinessCollection
{
get { return _businessCollection; }
set {
_businessCollection = value;
RaisePropertyOnChange("BusinessCollection");
}
}
How to get selected item of businesscollection in viewmodel?

1. One way to source binding:
You have to use SelectionChanged event. The easiest way is to write eventhandler in codebehind to "bind selecteditems" to viewmodel.
//ViewModel
public ICollectionView BusinessCollection {get; set;}
public List<YourBusinessItem> SelectedObject {get; set;}
//Codebehind
private void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var viewmodel = (ViewModel) DataContext;
viewmodel.SelectedItems = listview.SelectedItems
.Cast<YourBusinessItem>()
.ToList();
}
This still aligns with MVVM design, because view and viewmodel resposibilities are kept separated. You dont have any logic in codebehind and viewmodel is clean and testable.
2. Two way binding:
if you also need to update view, when viewmodel changes, you have to attach to ViewModel's PropertyChanged event and to the selected items' CollectionChanged event. of course you can do it in codebehind, but in this case I would create something more reusable:
//ViewModel
public ObservableCollection<YourBusinessItem> SelectedObject {get; set;}
//in codebehind:
var binder = new SelectedItemsBinder(listview, viewmodel.SelectedItems);
binder.Bind();
or can create custom attached property, so you can use binding syntax in xaml:
<ListView local:ListViewExtensions.SelectedValues="{Binding SelectedItem}" .../>
public class SelectedItemsBinder
{
private ListView _listView;
private IList _collection;
public SelectedItemsBinder(ListView listView, IList collection)
{
_listView = listView;
_collection = collection;
_listView.SelectedItems.Clear();
foreach (var item in _collection)
{
_listView.SelectedItems.Add(item);
}
}
public void Bind()
{
_listView.SelectionChanged += ListView_SelectionChanged;
if (_collection is INotifyCollectionChanged)
{
var observable = (INotifyCollectionChanged) _collection;
observable.CollectionChanged += Collection_CollectionChanged;
}
}
public void UnBind()
{
if (_listView != null)
_listView.SelectionChanged -= ListView_SelectionChanged;
if (_collection != null && _collection is INotifyCollectionChanged)
{
var observable = (INotifyCollectionChanged) _collection;
observable.CollectionChanged -= Collection_CollectionChanged;
}
}
private void Collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
foreach (var item in e.NewItems ?? new object[0])
{
if (!_listView.SelectedItems.Contains(item))
_listView.SelectedItems.Add(item);
}
foreach (var item in e.OldItems ?? new object[0])
{
_listView.SelectedItems.Remove(item);
}
}
private void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
foreach (var item in e.AddedItems ?? new object[0])
{
if (!_collection.Contains(item))
_collection.Add(item);
}
foreach (var item in e.RemovedItems ?? new object[0])
{
_collection.Remove(item);
}
}
}
Attached property implementation
public class ListViewExtensions
{
private static SelectedItemsBinder GetSelectedValueBinder(DependencyObject obj)
{
return (SelectedItemsBinder)obj.GetValue(SelectedValueBinderProperty);
}
private static void SetSelectedValueBinder(DependencyObject obj, SelectedItemsBinder items)
{
obj.SetValue(SelectedValueBinderProperty, items);
}
private static readonly DependencyProperty SelectedValueBinderProperty = DependencyProperty.RegisterAttached("SelectedValueBinder", typeof(SelectedItemsBinder), typeof(ListViewExtensions));
public static readonly DependencyProperty SelectedValuesProperty = DependencyProperty.RegisterAttached("SelectedValues", typeof(IList), typeof(ListViewExtensions),
new FrameworkPropertyMetadata(null, OnSelectedValuesChanged));
private static void OnSelectedValuesChanged(DependencyObject o, DependencyPropertyChangedEventArgs value)
{
var oldBinder = GetSelectedValueBinder(o);
if (oldBinder != null)
oldBinder.UnBind();
SetSelectedValueBinder(o, new SelectedItemsBinder((ListView)o, (IList)value.NewValue));
GetSelectedValueBinder(o).Bind();
}
public static void SetSelectedValues(Selector elementName, IEnumerable value)
{
elementName.SetValue(SelectedValuesProperty, value);
}
public static IEnumerable GetSelectedValues(Selector elementName)
{
return (IEnumerable)elementName.GetValue(SelectedValuesProperty);
}
}

Since the itemSource is BusinessCollection, you should be able to:
var selectedItems = BusinessCollection.Where(x => x.IsSelected);
Wrap it as a property on your VM.
Hope it helps!

Related

How to get instances that get edited from observable collection? not added or removed

There is a observable collection in my class and I want to be notified when the observable collection changes.
I searched on stackoverflow and found this code
private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (var item in e.OldItems)
{
//Removed items
}
}
else if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (var item in e.NewItems)
{
//Added items
}
}
else if(e.Action == NotifyCollectionChangedAction.Replace){
foreach (var item in e.NewItems)
{
}
}
}
Every thing was working fine. I get notified when something created and deleted. but I didnt get notified when an instance edited. what did I do wrong?
Edit: I changed the observable collection to binding list but nothing is happeng when I edit something.
<ListView Grid.Row="1" ItemsSource="{Binding Contacts}">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Text={Binding Name}></TextBox>
<TextBox Text={Binding Email}><TextBox>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
And my viewModel:
private BindingList<ContactViewModel> _contacts;
public IEnumerable<ContactViewModel> Contacts=>_contacts;
public ContactListing()
{
_contacts.ListChanged +=_contacts_ListChanged;
}
private void _contacts_ListChanged(object sender, ListChangedEventArgs e)
{
//Get Notified When somthing is edited
}
in the xaml I have some texboxes that when the text of them changes, The binding list or observable collection also changes(I set a breakpoint and the list was changing everytime something edited) but the ListChanged event doesnot call.
The collection element must implement INotifyPropertyChanged. And the collection needs to be replaced with BindingList< T>. Then in the ListChanged event you will receive a notification about the change in the properties of the elements of the collection. You will also have access to the AddingNew event, which occurs BEFORE an element is added to the collection.
It is also not working for editing. Its onliy working for creating and deleting.
Here is an example showing that it works when adding elements, and when changing any property of an element.
using Simplified;
namespace Core2023.SO.Eboy
{
public class ItemInpc : BaseInpc
{
private int _value;
private string title = string.Empty;
public int Id { get; }
public string Title { get => title; set => Set(ref title, value ?? string.Empty); }
public int Value { get => _value; set => Set(ref _value, value); }
public override string? ToString()
=> $"{Id}: {Title}-{Value}";
public ItemInpc(int id)
{
Id = id;
}
public ItemInpc() : this(-1) { }
}
}
using Simplified;
using System.ComponentModel;
using System.Linq;
using System.Text;
namespace Core2023.SO.Eboy
{
public class ItemsViewModel : BaseInpc
{
private string _listChangedArgs = string.Empty;
public BindingList<ItemInpc> Items { get; }
public ItemsViewModel()
{
Items = new BindingList<ItemInpc>(
"And the collection needs to be replaced"
.Split()
.Select((title, id) => new ItemInpc(id) { Title = title, Value = id * 23 })
.ToList());
Items.AddingNew += OnAddingNew;
Items.ListChanged += OnListChanged;
}
public string ListChangedArgs { get => _listChangedArgs; set => Set(ref _listChangedArgs, value); }
private void OnAddingNew(object? sender, AddingNewEventArgs e)
{
int id = Items.Max(it => it.Id) + 1;
ItemInpc item = new ItemInpc(id);
e.NewObject = item;
}
int i = 0;
private void OnListChanged(object? sender, ListChangedEventArgs e)
{
StringBuilder builder = new StringBuilder();
builder.Append(i);
i++;
builder.AppendLine(new string('-', 40));
builder.AppendLine(e.ListChangedType.ToString());
builder.AppendLine(e.PropertyDescriptor?.Name);
builder.AppendLine(e.NewIndex.ToString());
if (e.NewIndex < 0)
builder.AppendLine($"{e.NewIndex}");
else
builder.AppendLine($"{e.NewIndex} {Items[e.NewIndex]}");
if (e.OldIndex < 0)
builder.AppendLine($"{e.OldIndex}");
else
builder.AppendLine($"{e.OldIndex} {Items[e.OldIndex]}");
builder.AppendLine(ListChangedArgs);
ListChangedArgs = builder.ToString();
}
}
}
<Window x:Class="Core2023.SO.Eboy.ItemChangedWindow"
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:Core2023.SO.Eboy"
mc:Ignorable="d"
Title="ItemChangedWindow" Height="450" Width="800">
<Window.DataContext>
<local:ItemsViewModel/>
</Window.DataContext>
<UniformGrid Columns="2">
<DataGrid ItemsSource="{Binding Items}"/>
<TextBlock Text="{Binding ListChangedArgs}"/>
</UniformGrid>
</Window>
BaseInpc class from here: https://stackoverflow.com/a/67617794/13349759 .

How can I fire SelectionChanged event on MouseUp

I have a ListView, and I would like to change behaviour, so SelectionChanged event would fire on MouseUp, instead of MouseDown.
The reason why I want to change behaviour, is because when I want to drag objects (on MouseMove), the selection is changed when I click (MouseDown) on the objects to move.
I found that piece of code, but this is working only for simple selection, and I would like to allow selecting several items.
public static class SelectorBehavior
{
#region bool ShouldSelectItemOnMouseUp
public static readonly DependencyProperty ShouldSelectItemOnMouseUpProperty =
DependencyProperty.RegisterAttached(
"ShouldSelectItemOnMouseUp", typeof(bool), typeof(SelectorBehavior),
new PropertyMetadata(default(bool), HandleShouldSelectItemOnMouseUpChange));
public static void SetShouldSelectItemOnMouseUp(DependencyObject element, bool value)
{
element.SetValue(ShouldSelectItemOnMouseUpProperty, value);
}
public static bool GetShouldSelectItemOnMouseUp(DependencyObject element)
{
return (bool)element.GetValue(ShouldSelectItemOnMouseUpProperty);
}
private static void HandleShouldSelectItemOnMouseUpChange(
DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is Selector selector)
{
selector.PreviewMouseDown -= HandleSelectPreviewMouseDown;
selector.MouseUp -= HandleSelectMouseUp;
if (Equals(e.NewValue, true))
{
selector.PreviewMouseDown += HandleSelectPreviewMouseDown;
selector.MouseUp += HandleSelectMouseUp;
}
}
}
private static void HandleSelectMouseUp(object sender, MouseButtonEventArgs e)
{
var selector = (Selector)sender;
if (e.ChangedButton == MouseButton.Left && e.OriginalSource is Visual source)
{
var container = selector.ContainerFromElement(source);
if (container != null)
{
var index = selector.ItemContainerGenerator.IndexFromContainer(container);
if (index >= 0)
{
selector.SelectedIndex = index;
}
}
}
}
private static void HandleSelectPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
e.Handled = e.ChangedButton == MouseButton.Left;
}
#endregion
}
Here is my Xaml :
<ListView x:Name="ListViewContract" SelectedItem="{Binding SelectedContract}" ItemsSource="{Binding ListContractsView}" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="4" MouseDoubleClick="ListViewContract_DoubleClick" GridViewColumnHeader.Click ="GridViewHeaderClicked" SelectionChanged="ListViewContract_SelectionChanged" Visibility="{Binding Grid1Visible, Converter={StaticResource BoolToVisConverter}}"
MouseMove="ListViewContract_MouseMove" local:SelectorBehavior.ShouldSelectItemOnMouseUp="True">
<ListView.View>
<GridView AllowsColumnReorder="true" x:Name="GridViewContract">
<GridViewColumn DisplayMemberBinding="{Binding ID}" Header="ID" Width="{Binding WidthIDs, Mode=TwoWay}"/>
<GridViewColumn DisplayMemberBinding="{Binding Name}" Header="{x:Static p:Resources.Name}" Width="{Binding WidthColumnContractName, Mode=TwoWay}"/>
<GridViewColumn DisplayMemberBinding="{Binding Code}" Header="{x:Static p:Resources.Number}" Width="{Binding WidthColumnContractCode, Mode=TwoWay}"/>
<GridViewColumn DisplayMemberBinding="{Binding Comm}" Header="{x:Static p:Resources.Commentary}" Width="{Binding WidthColumnContractComm, Mode=TwoWay}"/>
<GridViewColumn DisplayMemberBinding="{Binding Revision}" Header="{x:Static p:Resources.Revision}" Width="{Binding WidthColumnContractRev, Mode=TwoWay}"/>
<GridViewColumn Header="{x:Static p:Resources.Advancement}" Width="{Binding WidthColumnContractAdv, Mode=TwoWay}">
<GridViewColumn.CellTemplate>
<DataTemplate>
<StackPanel>
<Rectangle Name="AFF_Track" Height="12" Stroke="black" StrokeThickness="1" Fill="{Binding RectangleProgression}" Tag="{Binding ID}" MouseMove="mouseOverProgressionContractAss">
<Rectangle.ToolTip>
<ContentControl Template="{StaticResource ToolTipOperations}"/>
</Rectangle.ToolTip>
</Rectangle>
<Rectangle Name="AFF_Track2" Height="12" Stroke="black" StrokeThickness="1" Fill="{Binding RectangleProgressionWeight}" Tag="{Binding ID}" MouseMove="mouseOverProgressionContractAss">
<Rectangle.ToolTip>
<ContentControl Template="{StaticResource ToolTipOperations}"/>
</Rectangle.ToolTip>
</Rectangle>
</StackPanel>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="{x:Static p:Resources.Advancement}" Width="{Binding WidthColumnContractAdvRep, Mode=TwoWay}">
<GridViewColumn.CellTemplate>
<DataTemplate>
<StackPanel>
<Rectangle Name="AFF_TrackRep" Height="12" Stroke="black" StrokeThickness="1" Fill="{Binding RectangleProgressionRep}" Tag="{Binding ID}" MouseMove="mouseOverProgressionContractRep">
<Rectangle.ToolTip>
<ContentControl Template="{StaticResource ToolTipOperations}"/>
</Rectangle.ToolTip>
</Rectangle>
<Rectangle Name="AFF_Track2Rep" Height="12" Stroke="black" StrokeThickness="1" Fill="{Binding RectangleProgressionRepWeight}" Tag="{Binding ID}" MouseMove="mouseOverProgressionContractRep">
<Rectangle.ToolTip>
<ContentControl Template="{StaticResource ToolTipOperations}"/>
</Rectangle.ToolTip>
</Rectangle>
</StackPanel>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn DisplayMemberBinding="{Binding Manager.CompleteName}" Header="{x:Static p:Resources.ProjectManager}" Width="{Binding WidthColumnContractManager, Mode=TwoWay}"/>
<GridViewColumn DisplayMemberBinding="{Binding Client.Name}" Header="{x:Static p:Resources.Customer}" Width="{Binding WidthColumnContractCustomer, Mode=TwoWay}"/>
<GridViewColumn DisplayMemberBinding="{Binding Source}" Header="{x:Static p:Resources.Source}" Width="{Binding WidthColumnContractSource, Mode=TwoWay}"/>
</GridView>
</ListView.View>
</ListView>
You can modify your behavior to handle controls that extend MultiSelector (like the DataGrid) and ListBox individually. ListBox (and therefore ListView too) is not a MultiSelector but supports multi selection when setting the ListBox.SelectionMode property to something different than SelectionMode.Single (which is the default).
For every other simple Selector you would have to track the selected items manually. You would also have to intercept the Selector.Unselected event to prevent the Selector from unselecting the selected items when in a multi select mode - Selector only supports single item selection.
The following example shows how you can track selected items in case the attached Selector is not a ListBox or a MultiSelector. For this reason the behavior exposes a public readonly SelectedItems dependency property.
The example also shows how to observe the pressed keyboard keys in order to filter multi select user actions. This example filters Shift or Ctrl keys as gesture to trigger the multi select behavior: random multi select while CTRL is pressed and range select while Shift key is pressed. Otherwise the Selector will behave as usual (single select on release of the left mouse button).
SelectorService.cs
public class SelectorService : DependencyObject
{
#region IsSelectItemOnMouseUpEnabled attached property
public static readonly DependencyProperty IsSelectItemOnMouseUpEnabledProperty = DependencyProperty.RegisterAttached(
"IsSelectItemOnMouseUpEnabled",
typeof(bool),
typeof(SelectorService),
new PropertyMetadata(default(bool), OnIsSelectItemOnMouseUpEnabledChanged));
public static void SetIsSelectItemOnMouseUpEnabled(DependencyObject attachedElement, bool value) => attachedElement.SetValue(IsSelectItemOnMouseUpEnabledProperty, value);
public static bool GetIsSelectItemOnMouseUpEnabled(DependencyObject attachedElement) => (bool)attachedElement.GetValue(IsSelectItemOnMouseUpEnabledProperty);
#endregion IsSelectItemOnMouseUpEnabled attached property
#region SelectedItems attached property
public static IList GetSelectedItems(DependencyObject attachedElement) => (IList)attachedElement.GetValue(SelectedItemsProperty);
public static void SetSelectedItems(DependencyObject attachedElement, IList value) => attachedElement.SetValue(SelectedItemsPropertyKey, value);
private static readonly DependencyPropertyKey SelectedItemsPropertyKey = DependencyProperty.RegisterAttachedReadOnly(
"SelectedItems",
typeof(IList),
typeof(SelectorService),
new PropertyMetadata(default));
public static readonly DependencyProperty SelectedItemsProperty = SelectedItemsPropertyKey.DependencyProperty;
#endregion SelectedItems attached property
#region SelectionMode attached property (private)
private static SelectionMode GetOriginalSelectionModeBackup(DependencyObject attachedElement) => (SelectionMode)attachedElement.GetValue(OriginalSelectionModeBackupProperty);
private static void SetOriginalSelectionModeBackup(DependencyObject attachedElement, SelectionMode value) => attachedElement.SetValue(OriginalSelectionModeBackupProperty, value);
private static readonly DependencyProperty OriginalSelectionModeBackupProperty = DependencyProperty.RegisterAttached(
"OriginalSelectionModeBackup",
typeof(SelectionMode),
typeof(SelectorService),
new PropertyMetadata(default));
#endregion SelectionMode attached property
private static bool IsRandomMultiSelectEngaged
=> Keyboard.Modifiers is ModifierKeys.Control;
private static bool IsRangeMultiSelectEngaged
=> Keyboard.Modifiers is ModifierKeys.Shift;
private static Dictionary<Selector, bool> IsMultiSelectAppliedMap { get; } = new Dictionary<Selector, bool>();
private static int SelectedRangeStartIndex { get; set; } = -1;
private static int SelectedRangeEndIndex { get; set; } = -1;
private static void OnIsSelectItemOnMouseUpEnabledChanged(DependencyObject attachedElement, DependencyPropertyChangedEventArgs e)
{
if (attachedElement is not Selector selector)
{
return;
}
if ((bool)e.NewValue)
{
WeakEventManager<FrameworkElement, MouseButtonEventArgs>.AddHandler(selector, nameof(selector.PreviewMouseLeftButtonDown), OnPreviewLeftMouseButtonDown);
WeakEventManager<FrameworkElement, MouseButtonEventArgs>.AddHandler(selector, nameof(selector.PreviewMouseLeftButtonUp), OnPreviewLeftMouseButtonUp);
if (selector.IsLoaded)
{
InitializeAttachedElement(selector);
}
else
{
selector.Loaded += OnSelectorLoaded;
}
}
else
{
WeakEventManager<FrameworkElement, MouseButtonEventArgs>.RemoveHandler(selector, nameof(selector.PreviewMouseLeftButtonDown), OnPreviewLeftMouseButtonDown);
WeakEventManager<FrameworkElement, MouseButtonEventArgs>.RemoveHandler(selector, nameof(selector.PreviewMouseLeftButtonUp), OnPreviewLeftMouseButtonUp);
SetSelectedItems(selector, null);
IsMultiSelectAppliedMap.Remove(selector);
selector.Loaded -= OnSelectorLoaded;
if (selector is ListBox listBox)
{
listBox.SelectionMode = GetOriginalSelectionModeBackup(listBox);
}
}
}
private static void OnSelectorLoaded(object sender, RoutedEventArgs e)
{
var selector = sender as Selector;
selector.Loaded -= OnSelectorLoaded;
InitializeAttachedElement(selector);
}
private static void InitializeAttachedElement(Selector selector)
{
IList selectedItems = new List<object>();
if (selector is ListBox listBox)
{
ValidateListBoxSelectionMode(listBox);
selectedItems = listBox.SelectedItems;
}
else if (selector is MultiSelector multiSelector)
{
selectedItems = multiSelector.SelectedItems;
}
else if (selector.SelectedItem is not null)
{
selectedItems.Add(selector.SelectedItem);
}
SetSelectedItems(selector, selectedItems);
IsMultiSelectAppliedMap.Add(selector, false);
}
private static void OnUnselected(object? sender, RoutedEventArgs e)
{
var itemContainer = sender as DependencyObject;
Selector.SetIsSelected(itemContainer, true);
Selector.RemoveUnselectedHandler(itemContainer, OnUnselected);
}
private static void OnPreviewLeftMouseButtonUp(object sender, MouseButtonEventArgs e)
{
var selector = sender as Selector;
DependencyObject itemContainerToSelect = ItemsControl.ContainerFromElement(selector, e.OriginalSource as DependencyObject);
DependencyObject currentSelectedItemContainer = selector.ItemContainerGenerator.ContainerFromItem(selector.SelectedItem);
if (itemContainerToSelect is null)
{
return;
}
if (IsRandomMultiSelectEngaged)
{
MultiSelectItems(selector, itemContainerToSelect, currentSelectedItemContainer);
}
else if (IsRangeMultiSelectEngaged)
{
MultiSelectRangeOfItems(selector, itemContainerToSelect, currentSelectedItemContainer);
}
else
{
SingleSelectItem(selector, itemContainerToSelect, currentSelectedItemContainer);
}
IsMultiSelectAppliedMap[selector] = IsRandomMultiSelectEngaged || IsRangeMultiSelectEngaged;
}
private static void MultiSelectRangeOfItems(Selector selector, DependencyObject itemContainerToSelect, DependencyObject currentSelectedItemContainer)
{
int clickedContainerIndex = selector.ItemContainerGenerator.IndexFromContainer(itemContainerToSelect);
// In case there is not any preselected item. Otherwis SingleSlectItem() has already set the SelectedRangeStartIndex property.
if (SelectedRangeStartIndex == -1)
{
SelectedRangeStartIndex = clickedContainerIndex;
DependencyObject itemContainer = selector.ItemContainerGenerator.ContainerFromIndex(SelectedRangeStartIndex);
MultiSelectItems(selector, itemContainer, currentSelectedItemContainer);
return;
}
// Complete the range selection
else if (SelectedRangeEndIndex == -1)
{
bool isSelectionRangeFromTopToBotton = clickedContainerIndex > SelectedRangeStartIndex;
if (isSelectionRangeFromTopToBotton)
{
SelectedRangeEndIndex = clickedContainerIndex;
}
else
{
// Selection is from bottom to top, so we need to swap start and end index
// as they are used to initialize the for-loop.
SelectedRangeEndIndex = SelectedRangeStartIndex;
SelectedRangeStartIndex = clickedContainerIndex;
}
for (int itemIndex = SelectedRangeStartIndex; itemIndex <= SelectedRangeEndIndex; itemIndex++)
{
DependencyObject itemContainer = selector.ItemContainerGenerator.ContainerFromIndex(itemIndex);
bool isContainerUnselected = !Selector.GetIsSelected(itemContainer);
if (isContainerUnselected)
{
MultiSelectItems(selector, itemContainer, currentSelectedItemContainer);
}
}
// Only remember start index to append sequential ranges (more clicks while Shift key is pressed)
// and invalidate the end index.
SelectedRangeEndIndex = -1;
}
}
private static void MultiSelectItems(Selector? selector, DependencyObject itemContainerToSelect, DependencyObject currentSelectedItemContainer)
{
bool oldIsSelectedValue = Selector.GetIsSelected(itemContainerToSelect);
// Toggle the current state
bool newIsSelectedValue = oldIsSelectedValue ^= true;
if (selector is ListBox listBox)
{
// In case the mode was overriden externally, force it back
// but store the changed value to allow roll back when the behavior gets disabled.
ValidateListBoxSelectionMode(listBox);
}
// If the current Selector instance does not support native multi select
// we need to prevent the Selector from unselecting previous selected items.
// By setting unselected items back to selected we can enforce a visual multi select feedback.
if (selector is not MultiSelector and not ListBox)
{
if (newIsSelectedValue && currentSelectedItemContainer is not null)
{
Selector.AddUnselectedHandler(currentSelectedItemContainer, OnUnselected);
}
}
Selector.SetIsSelected(itemContainerToSelect, newIsSelectedValue);
(itemContainerToSelect as UIElement)?.Focus();
if (selector is not MultiSelector and not ListBox)
{
object item = selector.ItemContainerGenerator.ItemFromContainer(itemContainerToSelect);
IList selectedItems = GetSelectedItems(selector);
if (newIsSelectedValue)
{
selectedItems.Add(item);
}
else
{
selectedItems.Remove(item);
}
}
}
private static void SingleSelectItem(Selector? selector, DependencyObject itemContainerToSelect, DependencyObject currentSelectedItemContainer)
{
bool isPreviousSelectMultiSelect = IsMultiSelectAppliedMap[selector];
if (!isPreviousSelectMultiSelect)
{
// Unselect the currently selected
if (currentSelectedItemContainer is not null)
{
Selector.SetIsSelected(currentSelectedItemContainer, false);
}
}
// If the Selector has multiple selected items and an item was clicked without the modifier key pressed,
// then we need to switch back to single selection mode and only select the currently clicked item.
else
{
// Invalidate tracked multi select range
SelectedRangeStartIndex = -1;
SelectedRangeEndIndex = -1;
if (selector is ListBox listBox)
{
ValidateListBoxSelectionMode(listBox);
listBox.UnselectAll();
}
else if (selector is MultiSelector multiSelector)
{
multiSelector.UnselectAll();
}
else
{
IList selectedItems = GetSelectedItems(selector);
foreach (object item in selectedItems)
{
DependencyObject itemContainer = selector.ItemContainerGenerator.ContainerFromItem(item);
Selector.SetIsSelected(itemContainer, false);
}
selectedItems.Clear();
}
}
// Execute single selection
Selector.SetIsSelected(itemContainerToSelect, true);
(itemContainerToSelect as UIElement)?.Focus();
if (selector is not MultiSelector and not ListBox)
{
IList selectedItems = GetSelectedItems(selector);
selectedItems.Clear();
selectedItems.Add(selector.SelectedItem);
}
// Track index in case the next click enabled select range (press Shift while click)
int clickedContainerIndex = selector.ItemContainerGenerator.IndexFromContainer(itemContainerToSelect);
SelectedRangeStartIndex = clickedContainerIndex;
return;
}
private static void ValidateListBoxSelectionMode(ListBox listBox)
{
if (listBox.SelectionMode is not SelectionMode.Extended)
{
// In case the mode was overriden externally, force it back
// but store the changed value to allow roll back when the behavior gets disabled.
SetOriginalSelectionModeBackup(listBox, listBox.SelectionMode);
listBox.SelectionMode = SelectionMode.Extended;
}
}
private static void OnPreviewLeftMouseButtonDown(object sender, MouseButtonEventArgs e)
=> e.Handled = true;
}
In addition to #BionicCode 's code : the code doesn't take in charge DoubleClick. I could solve it adding that code : (BionicCode, if you could edit answer for future visitors, thanks).
-Add a static variable in the class :
public static DateTime lastClickDateTime = DateTime.Now;
Then at the very beginning of SingleSelectItem function :
TimeSpan interval = DateTime.Now - lastClickDateTime;
if(interval.TotalMilliseconds<500)
{
if(selector is ListBox listBox)
{
MouseButtonEventArgs doubleClickEvent = new MouseButtonEventArgs(Mouse.PrimaryDevice, (int)DateTime.Now.Ticks, MouseButton.Left);
doubleClickEvent.RoutedEvent = Control.MouseDoubleClickEvent;
doubleClickEvent.Source = selector;
selector.RaiseEvent(doubleClickEvent);
return;
}
}
lastClickDateTime = DateTime.Now;
Thanks again for your help :)

Binding List of Items in WPF

I try to realize MVVM. TextBox text in View is Binded to property itemName in Model.
On view is DataGrid -> Binded to ViewModel.Rows property
In ViewModel on itemName on change event run async request to remote service for products, which is goes to model SugestProducts property. SugestProducts property is source for ListView items.
If products more than 0 listview open. ListView SelectedItem is Binded to model product property.
I need on product selection in list view fill itemName property from Product.name property without request to remote service. Other work good.
My model is:
public class RowDocumentSaleWraper : INotifyPropertyChanged
{
private ObservableCollection<Product> _sugestProducts;
public ObservableCollection<Product> SugestProducts
{
get
{
return _sugestProducts;
}
set
{
_sugestProducts = value;
NotifyPropertyChanged("SugestProducts");
}
}
public Product product {get; set;}
_itemName
public override string itemName
{
get
{
return itemName;
}
set
{
itemName = value;
NotifyPropertyChanged("itemName");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Product:
public class Product
{
public string name{get; set;}
}
My ViewModel Is:
public class OrderViewModel : DependencyObject
{
public ObservableCollection<RowDocumentSaleWraper> Rows { get; set; }
public OrderViewModel()
{
addNewRow();
}
internal void addNewRow()
{
RowDocumentSaleWraper row = new RowDocumentSaleWraper(Order);
row.PropertyChanged += row_PropertyChanged;
Rows.Add(row);
}
void row_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
RowDocumentSaleWraper row = sender as RowDocumentSaleWraper;
if (row != null && e.PropertyName == "itemName" && !String.IsNullOrEmpty(row.itemName))
{
//get products from remote service -> source for
requestProducts(row.itemName, row);
}
}
private async void requestProducts(string searchString, RowDocumentSaleWraper row)
{
if (!String.IsNullOrEmpty(searchString))
{
var products = await requestProductsAsync(searchString);
row.SugestProducts = listToObservable(products);
}
}
}
My Xaml:
<DataGrid Grid.Row="1" Name="mainDataGrid" ItemsSource="{Binding Rows , UpdateSourceTrigger=PropertyChanged}" AutoGenerateColumns="False" CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Product">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<TextBox PreviewKeyDown="TextBox_PreviewKeyDown" KeyDown="TextBox_KeyDown" Text="{Binding itemName, UpdateSourceTrigger=PropertyChanged}" MinWidth="200"/>
<ListView ItemsSource="{Binding SugestProducts, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
KeyDown="ListView_KeyDown" SelectedItem ="{Binding product, UpdateSourceTrigger=PropertyChanged}">
<ListView.View>
<GridView ColumnHeaderContainerStyle="{StaticResource myHeaderStyle}">
<GridViewColumn DisplayMemberBinding="{Binding code}"/>
<GridViewColumn DisplayMemberBinding="{Binding name}" />
</GridView>
</ListView.View>
</ListView>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
Based on the above comments, you should notify the ViewModel from the setter on itemName
public override string itemName
{
get
{
return itemName;
}
set
{
itemName = value;
NotifyPropertyChanged("itemName");
NotifyChange(itemName);
}
}
Then you'll define the event to retrieve the data
private async void NotifyChange(string name)
{
if (!String.IsNullOrEmpty(searchString))
{
var products = await requestProductsAsync(searchString);
SugestProducts = listToObservable(products);
}
}

How to select an item in LongListSelector using the MVVM-pattern?

I'm building application using the MVVM pattern. After clicking on one of the elements I want to see this element's details. I wrote this:
XAML
<phone:LongListSelector ItemsSource="{Binding Data}"
Margin="0,0,0,158"
SelectedItem="{Binding SelectedItem}">
<phone:LongListSelector.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Button>
<!-- Command="{Binding ShowDetailsAction}"-->
<Button.Template>
<ControlTemplate>
<TextBlock Text="{Binding Text}"></TextBlock>
</ControlTemplate>
</Button.Template>
</Button>
</StackPanel>
</DataTemplate>
</phone:LongListSelector.ItemTemplate>
</phone:LongListSelector>
ViewModel:
public IEnumerable SelectedItem
{
get { return _itemsControl; }
set
{
if (_itemsControl == value)
return;
_itemsControl = value;
// Test
_mss.ErrorNotification("fd");
}
}
I tried also using a command, which didn't work, too.
This was the command part:
public ICommand ShowDetailsCommand { get; private set; }
public ViewModel()
{
_loadDataCommand = new DelegateCommand(LoadDataAction);
SaveChangesCommand = new DelegateCommand(SaveChangesAction);
ShowDetailsCommand = new DelegateCommand(ShowDetailsAction);
}
private void ShowDetailsAction(object p)
{
_mss.ErrorNotification("bla bla");
}
EDIT
ViewModel
private IEnumerable _itemsControl;
public IEnumerable Data
{
get
{
return _itemsControl;
}
set
{
_itemsControl = value;
RaisePropertyChanged("Data");
}
}
protected void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Model
public string Text { get; set; }
public DateTimeOffset Data { get; set; }
EDIT2
private MobileServiceCollection<ModelAzure, ModelAzure> _items;
private readonly IMobileServiceTable<ModelAzure> _todoTable = App.MobileService.GetTable<ModelAzure>();
private async void RefreshTodoItems()
{
try
{
_items = await _todoTable.ToCollectionAsync();
}
catch (MobileServiceInvalidOperationException e)
{
_mss.ErrorNotification(e.ToString());
}
Data = _items;
}
Your Data property looks like
private MobileServiceCollection<ModelAzure, ModelAzure> _itemsControl;
public MobileServiceCollection<ModelAzure, ModelAzure> Data
{
get
{
return _itemsControl;
}
set
{
_itemsControl = value;
RaisePropertyChanged("Data");
}
}
Edited
It seems the SelectedItem property from LongListSelector cannot be bound in WP8.
What you can do is either :
Use the derived and fixed custom LongListSelector provided in the link above instead of the default one, which looks like :
public class LongListSelector : Microsoft.Phone.Controls.LongListSelector
{
public LongListSelector()
{
SelectionChanged += LongListSelector_SelectionChanged;
}
void LongListSelector_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
SelectedItem = base.SelectedItem;
}
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register(
"SelectedItem",
typeof(object),
typeof(LongListSelector),
new PropertyMetadata(null, OnSelectedItemChanged)
);
private static void OnSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var selector = (LongListSelector)d;
selector.SelectedItem = e.NewValue;
}
public new object SelectedItem
{
get { return GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
}
Register the SelectionChanged event from LongListSelector and call your ViewModel by yourself inside the associated handler/callback :
in your view :
<phone:LongListSelector x:Name="YourLongListSelectorName"
ItemsSource="{Binding Data}"
Margin="0,0,0,158"
SelectionChanged="OnSelectedItemChanged">
in your code behind :
private void OnSelectedItemChanged(object sender, SelectionChangedEventArgs selectionChangedEventArgs e)
{
((YourViewModel)this.DataContext).NewSelectedItemMethodOrWhateverYouWant((ModelAzure)this.YourLongListSelectorName.SelectedItem);
//or
((YourViewModel)this.DataContext).SelectedItem = (ModelAzure)this.YourLongListSelectorName.SelectedItem;
}
Finally your Button command wasn't properly working, because when you use a DataTemplate, the ambiant DataContext is the item itself. Which means that it was looking for your Command into your Model instance, not into your ViewModel instance.
Hope this helps
In your ViewModel, you have:
public IEnumerable SelectedItem
{
get { return _itemsControl; }
set
{
if (_itemsControl == value)
return;
_itemsControl = value;
// Test
_mss.ErrorNotification("fd");
}
}
Why is your SelectItem an IEnumerable? Should it not be of type "Model"? Your list is bound to "Data" which should be ObservableList, not IEnumerable. It will provide it's own change notification, so you don't need to.
The list will set the SelectedItem when it gets selected, but if the type is wrong, it won't get set.
Greg

Iterating selected items in Windows App Store Gridview

I know this is a long one, but please bear with me.
I have created a windows app store program very similar to Laurent Bugnion's "MyFriends" program in the MVVM light samples using the MVVM light framework.
In his program he uses the SelectedItem property of the gridview to keep track of which item is the selected item.
The problem is, I give the user the ability to select multiple items on the GridView and then operate on them using a button on the App Bar. For this SelectedItem will not work.
Does anyone know how to make this work with a multiselect GridView? I have tried the IsSelected property of the GridViewItem based on some articles on WPF, but this doesn't seem to work. The SelectedTimesheets getter always come back empty when called. Here is what I have so far:
MainPage.xaml (bound to a MainViewModel with a child TimesheetViewModel observable collection):
<GridView
x:Name="itemGridView"
IsItemClickEnabled="True"
ItemsSource="{Binding Timesheets}"
ItemTemplate="{StaticResource TimesheetTemplate}"
Margin="10"
Grid.Column="0"
SelectionMode="Multiple"
helpers:ItemClickCommand.Command="{Binding NavigateTimesheetCommand}" RenderTransformOrigin="0.738,0.55" >
<GridView.ItemContainerStyle>
<Style TargetType="GridViewItem">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
</Style>
</GridView.ItemContainerStyle>
</GridView>
MainViewModel (cut down from full code):
public class MainViewModel : ViewModelBase
{
private readonly IDataService _dataService;
private readonly INavigationService _navigationService;
/// <summary>
/// Initializes a new instance of the MainViewModel class.
/// </summary>
public MainViewModel(IDataService dataService, INavigationService navigationService)
{
_dataService = dataService;
_navigationService = navigationService;
Timesheets = new ObservableCollection<TimesheetViewModel>();
ExecuteRefreshCommand();
}
public ObservableCollection<TimesheetViewModel> Timesheets
{
get;
private set;
}
public IEnumerable<TimesheetViewModel> SelectedTimesheets
{
get { return Timesheets.Where(o => o.IsSelected); }
}
private async void ExecuteRefreshCommand()
{
var timesheets = await _dataService.GetTimesheets("domain\\user");
if (timesheets != null)
{
Timesheets.Clear();
foreach (var timesheet in timesheets)
{
Timesheets.Add(new TimesheetViewModel(timesheet));
}
}
}
}
TimesheetViewModel:
public class TimesheetViewModel: ViewModelBase
{
public bool IsSelected { get; set; }
public Timesheet Model
{
get;
private set;
}
public TimesheetViewModel(Timesheet model)
{
Model = model;
}
}
If I set the IsSelected property manually, the SelectedTimesheets lambda works, so the problem is somewhere in the binding of the XAML to the IsSelected property.
Any help would be appreciated.
Sure, I know what you mean. Too bad this isn't automagic, but it isn't. The solution involves a simple custom GridView that inherits from GridView. Nothing too crazy, that is, if you let it sink in. Here's the code, I just tested it:
Here's your XAML:
<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
<Grid.ColumnDefinitions >
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<local:MyGridView ItemsSource="{Binding Items}" SelectionMode="Multiple"
BindableSelectedItems="{Binding Selected}" />
<local:MyGridView Grid.Column="1" ItemsSource="{Binding Selected}" />
</Grid>
Here's your view model (super-simplified):
public class ViewModel
{
ObservableCollection<string> m_Items
= new ObservableCollection<string>(Enumerable.Range(1, 100).Select(x => x.ToString()));
public ObservableCollection<string> Items { get { return m_Items; } }
ObservableCollection<object> m_Selected = new ObservableCollection<object>();
public ObservableCollection<object> Selected { get { return m_Selected; } }
}
And here's your custom gridview:
public class MyGridView : GridView
{
public ObservableCollection<object> BindableSelectedItems
{
get { return GetValue(BindableSelectedItemsProperty) as ObservableCollection<object>; }
set { SetValue(BindableSelectedItemsProperty, value as ObservableCollection<object>); }
}
public static readonly DependencyProperty BindableSelectedItemsProperty =
DependencyProperty.Register("BindableSelectedItems",
typeof(ObservableCollection<object>), typeof(MyGridView),
new PropertyMetadata(null, (s, e) =>
{
(s as MyGridView).SelectionChanged -= (s as MyGridView).MyGridView_SelectionChanged;
(s as MyGridView).SelectionChanged += (s as MyGridView).MyGridView_SelectionChanged;
}));
void MyGridView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (BindableSelectedItems == null)
return;
foreach (var item in BindableSelectedItems.Where(x => !this.SelectedItems.Contains(x)).ToArray())
BindableSelectedItems.Remove(item);
foreach (var item in this.SelectedItems.Where(x => !BindableSelectedItems.Contains(x)))
BindableSelectedItems.Add(item);
}
}
Just one new property BindableSelectedItems.
Best of luck!
#Jerry-Nixon-MSFT's answer spurred me on to rethink it (thanks to him) and I came up with the following solution.
Firstly I changed the XAML to accept a new helper method SelectionChangedCommand.Command and bound it to a RelayCommand called SelectionChangedCommand in my view model
MainPage.xaml
<GridView
x:Name="itemGridView"
IsItemClickEnabled="True"
ItemsSource="{Binding Timesheets}"
ItemTemplate="{StaticResource TimesheetTemplate}"
Margin="10"
Grid.Column="0"
SelectionMode="Multiple"
helpers:ItemClickCommand.Command="{Binding NavigateTimesheetCommand}"
helpers:SelectionChangedCommand.Command="{Binding SelectionChangedCommand}
"RenderTransformOrigin="0.738,0.55" >
</GridView>
I then added a SelectionChangedCommand helper class under my helpers namespace to translate the SelectionChanged event into an ICommand
namespace TimesheetManager.Helpers
{
public class SelectionChangedCommand
{
public static readonly DependencyProperty CommandProperty =
DependencyProperty.RegisterAttached("Command", typeof(ICommand),
typeof(SelectionChangedCommand), new PropertyMetadata(null,
OnCommandPropertyChanged));
public static void SetCommand(DependencyObject d, ICommand value)
{
d.SetValue(CommandProperty, value);
}
public static ICommand GetCommand(DependencyObject d)
{
return (ICommand)d.GetValue(CommandProperty);
}
private static void OnCommandPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var control = d as ListViewBase;
if (control != null)
control.SelectionChanged += OnSelectionChanged;
}
private static void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var control = sender as ListViewBase;
var command = GetCommand(control);
if (command != null && command.CanExecute(e))
command.Execute(e);
}
}
}
This binds the SelectionChanged event of any control which inherits from ListViewBase (our gridview) to a method called OnSelectionChanged. OnSelectionChanged subsequently passes the SelectionChangedEventArgs from the control to the RelayCommand binding in the XAML.
Finally in MainViewModel, I process the RelayCommand and set the IsSelected flag:
MainViewModel:
private RelayCommand<object> _selectionChangedCommand;
/// <summary>
/// Gets the SelectionChangedCommand.
/// </summary>
public RelayCommand<object> SelectionChangedCommand
{
get
{
return _selectionChangedCommand ?? (_selectionChangedCommand = new RelayCommand<object>
((param) => ExecuteSelectionChangedCommand(param)));
}
}
private void ExecuteSelectionChangedCommand(object sender)
{
var x = sender as SelectionChangedEventArgs;
foreach (var item in x.AddedItems)
((TimesheetViewModel)item).IsSelected = true;
foreach (var item in x.RemovedItems)
((TimesheetViewModel)item).IsSelected = false;
}
I know there is a fair amount of casting going on, but we are limited to object by the ICommand interface.
Hope this helps.

Categories

Resources