DataGrid.SelectedItems MVVM - c#

I developed my UI on MVVM pattern and now stuck on getting SelectedItems. Could you please modify my XAML and provide sample how do I get them insde ViewModel class.
<xcdg:DataGridControl Name="ResultGrid" ItemsSource="{Binding Results}" Height="295" HorizontalAlignment="Left" Margin="6,25,0,0" VerticalAlignment="Top" Width="1041" ReadOnly="True">
<xcdg:DataGridControl.View>
<xcdg:TableflowView UseDefaultHeadersFooters="False">
<xcdg:TableflowView.FixedHeaders>
<DataTemplate>
<xcdg:ColumnManagerRow />
</DataTemplate>
</xcdg:TableflowView.FixedHeaders>
</xcdg:TableflowView>
</xcdg:DataGridControl.View>
</xcdg:DataGridControl>

You can use Attached behaviors to get/set SelectedItems to datagrid.
I was facing similar issue in Metro apps, So had to write it myself.
Below is the link
http://www.codeproject.com/Articles/412417/Managing-Multiple-selection-in-View-Model-NET-Metr
Though i had written for metro apps, the same solution can be adapted in WPF/Silverlight.
public class MultiSelectBehavior : Behavior<ListViewBase>
{
#region SelectedItems Attached Property
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(
"SelectedItems",
typeof(ObservableCollection<object>),
typeof(MultiSelectBehavior),
new PropertyMetadata(new ObservableCollection<object>(), PropertyChangedCallback));
#endregion
#region private
private bool _selectionChangedInProgress; // Flag to avoid infinite loop if same viewmodel is shared by multiple controls
#endregion
public MultiSelectBehavior()
{
SelectedItems = new ObservableCollection<object>();
}
public ObservableCollection<object> SelectedItems
{
get { return (ObservableCollection<object>)GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectionChanged += OnSelectionChanged;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.SelectionChanged -= OnSelectionChanged;
}
private static void PropertyChangedCallback(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
NotifyCollectionChangedEventHandler handler = (s, e) => SelectedItemsChanged(sender, e);
if (args.OldValue is ObservableCollection<object>)
{
(args.OldValue as ObservableCollection<object>).CollectionChanged -= handler;
}
if (args.NewValue is ObservableCollection<object>)
{
(args.NewValue as ObservableCollection<object>).CollectionChanged += handler;
}
}
private static void SelectedItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (sender is MultiSelectBehavior)
{
var listViewBase = (sender as MultiSelectBehavior).AssociatedObject;
var listSelectedItems = listViewBase.SelectedItems;
if (e.OldItems != null)
{
foreach (var item in e.OldItems)
{
if (listSelectedItems.Contains(item))
{
listSelectedItems.Remove(item);
}
}
}
if (e.NewItems != null)
{
foreach (var item in e.NewItems)
{
if (!listSelectedItems.Contains(item))
{
listSelectedItems.Add(item);
}
}
}
}
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_selectionChangedInProgress) return;
_selectionChangedInProgress = true;
foreach (var item in e.RemovedItems)
{
if (SelectedItems.Contains(item))
{
SelectedItems.Remove(item);
}
}
foreach (var item in e.AddedItems)
{
if (!SelectedItems.Contains(item))
{
SelectedItems.Add(item);
}
}
_selectionChangedInProgress = false;
}
}

There is probably more to do if you want a multiselection and you want to get those selected items. Do you want to store the selected items and when some action is performed (button clicked or something like that) you want to use those selectedItems and do something with them?
There is a good example on that available here:
Get SelectedItems From DataGrid Using MVVM
It states it is designed for Silverlight, but it will work in WPF with MVVM too.
Perhaps this is a more straightforward approach:
Get Selected items in a WPF datagrid

Creating an attached behavior for wiring up every Read Only Collection or non Dependency property would take a significant amount of work. A simple solution is to pass the reference to the view model using the view.
Private ReadOnly Property ViewModel As MyViewModel
Get
Return DirectCast(DataContext, MyViewModel)
End Get
End Property
Private Sub MyView_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
If ViewModel.SelectedItems Is Nothing Then
ViewModel.SelectedItems = MyDataGrid.SelectedItems
End If
End Sub

Related

WPF Select multiple items in DataGrid from ViewModel with twoway binding

I'm trying to create a twoway binding to the selected items in a WPF DataGrid. I want to be able to select multiple items from the ViewModel and also from the actual UserControl. I know I cannot directly set the selected items since this property is readonly.
I'm thinking of binding to a DependencyProperty and subscribing to the SelectionChanged event of the DataGrid in the code behind.
<UserControl.Resources>
<Style TargetType="local:ListView">
<Setter Property="SelectedItems" Value="{Binding SelectedItemsVM, Mode=TwoWay}"/>
</Style>
</UserControl.Resources>
<DataGrid Name="ObjectListDataGrid" SelectionChanged="OnSelectionChanged">
In the code behind I create the DependencyProperty. When this is set, I subscribe to the CollectionChanged event.
public ObservableCollection<object> SelectedItems
{
get { return (ObservableCollection<object>)GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register("SelectedItems", typeof(ObservableCollection<object>), typeof(ListView), new PropertyMetadata(default(ObservableCollection<object>), OnSelectedItemsChanged));
private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var collection = e.NewValue as ObservableCollection<object>;
if (collection != null)
{
collection.CollectionChanged -= OnSelectedItemsCollectionChanged;
collection.CollectionChanged += OnSelectedItemsCollectionChanged;
}
}
I use the EventHandler to the SelectionChanged event of the DataGrid to add/remove the items in the collection.
private void OnSelectionChanged(object sender, SelectionChangedEventArgs args)
{
SelectedItems.Remove(args.RemovedItems);
SelectedItems.Add(args.AddedItems);
}
Now I want to select the rows needed in the OnSelectedItemsCollectionChanged method when the collection changes.
private static void OnSelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
foreach (DataGridRow row in ObjectListDataGrid.Rows())
{
if(ObjectListDataGrid.SelectedItems.Contains(row.Item))
row.IsSelected = true;
else
row.IsSelected = false;
}
}
The problem:
Since the OnSelectedItemsCollectionChanged method is static, I do not have access to the ObjectListDataGrid. Is there any way to overcome this, or do it in a different way?
For completeness, the DataGrid.Rows() method is an extension method to get a list of the rows:
public static IEnumerable<DataGridRow> Rows(this DataGrid grid)
{
var itemsSource = grid.ItemsSource as IEnumerable;
if (null == itemsSource) yield return null;
foreach (var item in itemsSource)
{
var row = grid.ItemContainerGenerator.ContainerFromItem(item) as DataGridRow;
if (null != row) yield return row;
}
}
Your OnSelectedItemsCollectionChanged method must not be static.
Cast the DependencyObject argument of the OnSelectedItemsChanged method to your ListView class. Also make sure to detach the handler from the old value of the SelectedItems property.
private static void OnSelectedItemsChanged(
DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var listView = (ListView)d;
var oldCollection = (INotifyCollectionChanged)e.OldValue;
var newCollection = (INotifyCollectionChanged)e.NewValue;
if (oldCollection != null)
{
oldCollection.CollectionChanged -= listView.OnSelectedItemsCollectionChanged;
}
if (newCollection != null)
{
newCollection.CollectionChanged += listView.OnSelectedItemsCollectionChanged;
}
}
private void OnSelectedItemsCollectionChanged(
object sender, NotifyCollectionChangedEventArgs e)
{
foreach (var row in ObjectListDataGrid.Rows())
{
row.IsSelected = ObjectListDataGrid.SelectedItems.Contains(row.Item);
}
}

Interaction behaviour not being called with ListBox's ItemSource binding

This is my first foray into WPF and I've been trying to follow MVVM closely to get things right. The context here is that I've got a view that should display different sets of messages, all of which are stored in ObservableCollection<T>.
This is the code in my View (it's a UserControl that's hosted by a different view so I can navigate between different views at runtime)
The "i" namespace is xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
<ListBox ItemsSource="{Binding Messages}">
<i:Interaction.Behaviors>
<behaviours:ScrollOnNewItemBehaviour />
</i:Interaction.Behaviors>
<ListBox.ItemTemplate>
<DataTemplate DataType="entities:DisplayedUserMessage">
<!-- Removed for brevity -->
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
This is the code for the behaviour (pieced together from other SO questions I was browsing while getting to grips with the concept):
public sealed class ScrollOnNewItemBehaviour : Behavior<ListBox>
{
protected override void OnAttached()
{
AssociatedObject.Loaded += OnLoaded;
AssociatedObject.Unloaded += OnUnLoaded;
}
protected override void OnDetaching()
{
AssociatedObject.Loaded -= OnLoaded;
AssociatedObject.Unloaded -= OnUnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
var incc = AssociatedObject.ItemsSource as INotifyCollectionChanged;
if (incc == null) return;
incc.CollectionChanged += OnCollectionChanged;
}
private void OnUnLoaded(object sender, RoutedEventArgs e)
{
var incc = AssociatedObject.ItemsSource as INotifyCollectionChanged;
if (incc == null) return;
incc.CollectionChanged -= OnCollectionChanged;
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
var border = (Border)VisualTreeHelper.GetChild(AssociatedObject, 0);
var scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
// Only scroll when we're scrolled to the bottom of the listbox
if (scrollViewer.VerticalOffset == scrollViewer.ScrollableHeight)
{
scrollViewer.ScrollToBottom();
}
}
}
}
So here is where my specific issue comes up- the binding works just fine. When I change _selectedChannel (I've removed irrelevant code from the ViewModel below) the view updates with the new messages (_messages is a dictionary that holds the various ObservableCollection instances) and when I add new messages to them, the UI updates as well.
The problem is that at no point does the behaviour I've registered to the ListBox get triggered, which is a bit of an issue since I'm relying on it to keep things scrolled. My best guess was that maybe it doesn't support a bound ItemSource and the fact that the ItemSource is initially null (the dictionary will be populated asynchronously so there is no default set) means that it doesn't get registered properly/needs to be re-registered every time the binding updates?
public MessagesViewModel : ViewModelBase
{
private ObservableCollection<DisplayedUserMessage> _displayedMessages;
private Channel _selectedChannel;
public IList<DisplayedUserMessage> Messages
{
get
{
return _displayedMessages;
}
set
{
if (_displayedMessages == value)
{
return;
}
_displayedMessages = value;
NotifyPropertyChanged();
}
}
public Channel SelectedChannel
{
get
{
return _selectedChannel;
}
set
{
if (_selectedChannel == value)
{
return;
}
_selectedChannel = value;
Messages = _messages[_selectedChannel.Id];
NotifyPropertyChanged();
}
}
}
The behaviour works if it gets executed (I've verified that it doesn't with breakpoints), so if anyone has an idea regarding what I should change to make this work with changing ItemSources, please let me know!
You can subscribe to the PropertyChanged event of AssociatedObject.DataContext and wait for the Messages property to change to a valid value.
Or you initialize Messages with an empty collection and once the items are created, add them to Messages (this would raise the CollectionChanged for each item added).
A third solution is to subscribe to the ItemsControl.ItemContainerGenerator.ItemsChanged event. It is raised whenever the ItemsControl.Items property or the collection changes.
You can use this event instead of relying on the ItemsSource binding source to implement INotifyCollectionChanged. You also don't need to require the DataContext to implement INotifyPropertyChanged.
This will make your behavior more generic and reusable as it can now work with any ItemsControl.
This means listening to ItemsControl.ItemContainerGenerator.ItemsChanged is equal to INotifyCollectionChanged.CollectionChanged:
private void OnLoaded(object sender, RoutedEventArgs e)
{
var itemsControl = AssociatedObject as ItemsControl;
if (itemsControl == null) return;
itemsControl.ItemContainerGenerator.ItemsChanged += OnCollectionChanged;
}
private void OnCollectionChanged(object sender, ItemsChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
var border = (Border)VisualTreeHelper.GetChild(AssociatedObject, 0);
var scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
// Only scroll when we're scrolled to the bottom of the listbox
if (scrollViewer.VerticalOffset == scrollViewer.ScrollableHeight)
{
scrollViewer.ScrollToBottom();
}
}
}

Creating Items DP for charting user control

I am busy creating a user control that has some basic charting/graph functions. In essence I want to have an "Items" dependency property to which the user of the control can bind. The control will then display all the items and updates made to the source.
What I have done so far was to create an "Items" DP in my user control, code behind.
public static readonly DependencyProperty ItemsProperty =
DependencyProperty.Register("Items",
typeof(ObservableCollection<Polyline>),
typeof(RPGraph),
new FrameworkPropertyMetadata(
new ObservableCollection<Polyline>(),
new PropertyChangedCallback(OnItemsChanged)));
public ObservableCollection<Polyline> Items
{
get { return (ObservableCollection<Polyline>)GetValue(ItemsProperty); }
set { SetValue(ItemsProperty, value); }
}
public static void OnItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
}
My first stumbling block was that "OnItemsChanged" didn't get called when my collection changed. After a couple of hours I found a stackoverflow post explaining why (ObservableCollection dependency property does not update when item in collection is deleted). Following this advice solved one part of my problem. Now I could Add new items (Polylines) to the ObservableCollection list. But what if I added an extra point or modified a point in the Polyline. Armed with the knowledge of the previous problem I found the Points.Changed event. I then subscribed to it and placed the update code in there.
This finally works, but man there must be a better or more elegant way of achieving this (as stated at the top), which I think all boils down to not using ObservableCollection? Any advice?
Below is the working OnItemChanged method (excuse the draft code, I just wanted to get it working :-) :
public static void OnItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var thisControl = d as RPGraph;
foreach (Polyline poly in thisControl.Items)
thisControl.manager.Items.Add(poly.Points.ToArray());
if (e.OldValue != null)
{
var coll = (INotifyCollectionChanged)e.OldValue;
// Unsubscribe from CollectionChanged on the old collection
coll.CollectionChanged -= Items_CollectionChanged;
}
if (e.NewValue != null)
{
var coll = (ObservableCollection<Polyline>)e.NewValue;
// Subscribe to CollectionChanged on the new collection
coll.CollectionChanged += (o, t) => {
ObservableCollection<Polyline> items = o as ObservableCollection<Polyline>;
thisControl.manager.Items.Add(items[t.NewStartingIndex].Points.ToArray());
foreach (Polyline poly in items)
{
poly.Points.Changed += (n, m) => {
for (int i = 0; i < thisControl.manager.Items.Count; i++)
thisControl.manager.Items[i] = thisControl.Items[i].Points.ToArray();
thisControl.manager.DrawGraph(thisControl.graphView);
};
}
thisControl.manager.DrawGraph(thisControl.graphView);
};
}
thisControl.manager.DrawGraph(thisControl.graphView);
}
You are completely right, an ObservableCollection does not notify when any of its items changes its property value.
You could extend the functionality of ObservableCollection adding notifications for these cases.
It may look like this:
public sealed class ObservableNotifiableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
public event ItemPropertyChangedEventHandler ItemPropertyChanged;
public event EventHandler CollectionCleared;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
{
base.OnCollectionChanged(args);
if (args.NewItems != null)
{
foreach (INotifyPropertyChanged item in args.NewItems)
{
item.PropertyChanged += this.OnItemPropertyChanged;
}
}
if (args.OldItems != null)
{
foreach (INotifyPropertyChanged item in args.OldItems)
{
item.PropertyChanged -= this.OnItemPropertyChanged;
}
}
}
protected override void ClearItems()
{
foreach (INotifyPropertyChanged item in this.Items)
{
item.PropertyChanged -= this.OnItemPropertyChanged;
}
base.ClearItems();
this.OnCollectionCleared();
}
private void OnCollectionCleared()
{
EventHandler eventHandler = this.CollectionCleared;
if (eventHandler != null)
{
eventHandler(this, EventArgs.Empty);
}
}
private void OnItemPropertyChanged(object sender, PropertyChangedEventArgs args)
{
ItemPropertyChangedEventHandler eventHandler = this.ItemPropertyChanged;
if (eventHandler != null)
{
eventHandler(this, new ItemPropertyChangedEventArgs(sender, args.PropertyName));
}
}
}
Then you can subscribe to the ItemPropertyChanged event and do your stuff.

Raise/Fire Listbox SelectionChangedEvent manually

I want to select programatically multiple items in my ListBox. So, to be as mvvm friendly as possible, I create a custom control inherited from ListBox. In this custom control I've made a dependency property allowing items selection changes. Here is the code of the OnPropertyChanged part :
private static void OnSetSelectionToPropertyChanged
(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
InitializableListBox list = d as InitializableListBox;
Dictionary<int, string> toSelect = e.NewValue as Dictionary<int, string>;
if (toSelect == null)
return;
list.SetSelectedItems(toSelect);
}
The selection works great, but this solution does not raise the OnSelectionChanged event
So I try also :
private static void OnSetSelectionToPropertyChanged
(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
InitializableListBox list = d as InitializableListBox;
Dictionary<int, string> toSelect = e.NewValue as Dictionary<int, string>;
if (toSelect == null)
return;
SelectionChangedEventArgs e_selChanged;
List<Object> removed = new List<object>();
List<Object> added = new List<object>();
//Clear the SelectedItems list
while(list.SelectedItems.Count > 0)
{
removed.Add(list.SelectedItems[0]);
list.SelectedItems.RemoveAt(0);
}
//Add each selected items
foreach (var item in toSelect)
{
list.SelectedItems.Add(item);
added.Add(list.SelectedItems[list.SelectedItems.Count - 1]);
}
//Raise the SelectionChanged event
e_selChanged = new SelectionChangedEventArgs(SelectionChangedEvent,removed,added);
list.OnSelectionChanged(e_selChanged);
}
But this was not better. I think I'm not dealing the right way with the event so if you could help me, that would be great.
Thanks in advance.
EDIT
I have found another solution (more a hack actually) than #NETscape. I don't think it's better, but it seems to work pretty well, and it's maybe easier.
The trick is to made a dependency property wich allow you to access the SelectedItems property (wich is readonly and unbindable on the normal ListBox). Here is the code of my custom ListBox :
public class InitializableListBox : ListBox
{
public InitializableListBox()
{
SelectionChanged += CustomSelectionChanged;
}
private void CustomSelectionChanged(object sender, SelectionChangedEventArgs e)
{
InitializableListBox s = sender as InitializableListBox;
if (s == null)
return;
s.CustomSelectedItems = s.SelectedItems;
}
#region CustomSelectedItems DependyProperty
public static DependencyProperty CustomSelectedItemsProperty =
DependencyProperty.RegisterAttached("CustomSelectedItems",
typeof(System.Collections.IList),typeof(InitializableListBox),
new PropertyMetadata(null, OnCustomSelectedItemsPropertyChanged));
public System.Collections.IList CustomSelectedItems
{
get
{
return (System.Collections.IList)GetValue(CustomSelectedItemsProperty);
}
set
{
SetValue(CustomSelectedItemsProperty, value);
}
}
private static void OnCustomSelectedItemsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
InitializableListBox list = d as InitializableListBox;
System.Collections.IList toSelect = e.NewValue as System.Collections.IList;
if (toSelect == null)
return;
list.SetSelectedItems(toSelect);
}
#endregion
}
See this
If you set your ItemsSource to a collection, and the objects in the collection implement INPC, you can set the Style on ListViewItem to use the bound objects IsSelected property.
See this answer to understand what I mean.
Let's say you have ItemsSource="{Binding Items}", then you can do something like:
Items.Where(item => item.IsSelected == true);
to return your list of items that are selected.
You could also do Items[0].IsSelected = true; to programmatically select an item.
In short, you shouldn't have to use a custom control to implement multiple selection on a ListView.
EDIT
I experienced the virtualization problem when I was implementing Telerik's RadGridView. I used a behavior to counter this problem and it seems to have worked:
public class RadGridViewExt : Behavior<RadGridView>
{
private RadGridView Grid
{
get
{
return AssociatedObject as RadGridView;
}
}
public INotifyCollectionChanged SelectedItems
{
get { return (INotifyCollectionChanged)GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register("SelectedItems", typeof(INotifyCollectionChanged), typeof(RadGridViewExt), new PropertyMetadata(OnSelectedItemsPropertyChanged));
private static void OnSelectedItemsPropertyChanged(DependencyObject target, DependencyPropertyChangedEventArgs args)
{
var collection = args.NewValue as INotifyCollectionChanged;
if (collection != null)
{
collection.CollectionChanged += ((RadGridViewExt)target).ContextSelectedItemsCollectionChanged;
}
}
protected override void OnAttached()
{
base.OnAttached();
Grid.SelectedItems.CollectionChanged += GridSelectedItemsCollectionChanged;
}
void ContextSelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
UnsubscribeFromEvents();
Transfer(SelectedItems as IList, AssociatedObject.SelectedItems);
SubscribeToEvents();
}
void GridSelectedItemsCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
UnsubscribeFromEvents();
Transfer(AssociatedObject.SelectedItems, SelectedItems as IList);
SubscribeToEvents();
}
private void SubscribeToEvents()
{
AssociatedObject.SelectedItems.CollectionChanged += GridSelectedItemsCollectionChanged;
if (SelectedItems != null)
{
SelectedItems.CollectionChanged += ContextSelectedItemsCollectionChanged;
}
}
private void UnsubscribeFromEvents()
{
AssociatedObject.SelectedItems.CollectionChanged -= GridSelectedItemsCollectionChanged;
if (SelectedItems != null)
{
SelectedItems.CollectionChanged -= ContextSelectedItemsCollectionChanged;
}
}
public static void Transfer(IList source, IList target)
{
if (source == null || target == null)
return;
target.Clear();
foreach (var o in source)
{
target.Add(o);
}
}
}
Inside RadGridView control:
<i:Interaction.Behaviors>
<local:RadGridViewExt SelectedItems="{Binding SelectedItems}" />
</i:Interaction.Behaviors>
where
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
and remember to add reference to Interactivity assembly.
ListBox has a property called SelectionMode. You can set it to Multiple to enable multiple selection.
<ListBox x:Name="MyListBox" SelectionMode="Multiple"/>
You can use SelectedItems property to get all the selected items afterwards.

How to control the scroll position of a ListBox in a MVVM WPF app

I have got a big ListBox with vertical scrolling enabled, my MVVM has New and Edit ICommands.
I am adding new item to the end of the collection but I want the scrollbar also to auto position to the End when I call my MVVM-AddCommand.
I am also making an item editable(By calling EditCommand with a particular row item) from some other part of the application so that my ListBoxItem getting in to edit mode using DataTrigger, but how will I bring that particular row(ListBoxItem) to the view by adjusting the scroll position.
If I am doing it in the View side I can call listBox.ScrollInToView(lstBoxItem).
But what is the best way to solve this common Scroll issue from an MVVM perspective.
I typically set IsSynchronizedWithCurrentItem="True" on the ListBox. Then I add a SelectionChanged handler and always bring the selected item into view, with code like this:
private void BringSelectionIntoView(object sender, SelectionChangedEventArgs e)
{
Selector selector = sender as Selector;
if (selector is ListBox)
{
(selector as ListBox).ScrollIntoView(selector.SelectedItem);
}
}
From my VM I can get the default collection view and use one of the MoveCurrent*() methods to ensure that the item being edited is the current item.
CollectionViewSource.GetDefaultView(_myCollection).MoveCurrentTo(thisItem);
NOTE: Edited to use ListBox.ScrollIntoView() to accomodate virtualization
Using this in MVVM can be easily accomplished via an attached behavior like so:
using System.Windows.Controls;
using System.Windows.Interactivity;
namespace Jarloo.Sojurn.Behaviors
{
public sealed class ScrollIntoViewBehavior : Behavior<ListBox>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectionChanged += ScrollIntoView;
}
protected override void OnDetaching()
{
AssociatedObject.SelectionChanged -= ScrollIntoView;
base.OnDetaching();
}
private void ScrollIntoView(object o, SelectionChangedEventArgs e)
{
ListBox b = (ListBox) o;
if (b == null)
return;
if (b.SelectedItem == null)
return;
ListBoxItem item = (ListBoxItem) ((ListBox) o).ItemContainerGenerator.ContainerFromItem(((ListBox) o).SelectedItem);
if (item != null) item.BringIntoView();
}
}
}
Then in the View ad this reference at the top:
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
And do this:
<ListBox ItemsSource="{Binding MyData}" SelectedItem="{Binding MySelectedItem}">
<i:Interaction.Behaviors>
<behaviors:ScrollIntoViewBehavior />
</i:Interaction.Behaviors>
</ListBox>
Now when the SelectedItem changes the behavior will do the BringIntoView() call for you.
This is the attached property form of the accepted answer:
using System.Windows;
using System.Windows.Controls;
namespace CommonBehaviors
{
public static class ScrollCurrentItemIntoViewBehavior
{
public static readonly DependencyProperty AutoScrollToCurrentItemProperty =
DependencyProperty.RegisterAttached("AutoScrollToCurrentItem",
typeof(bool), typeof(ScrollCurrentItemIntoViewBehavior),
new UIPropertyMetadata(default(bool), OnAutoScrollToCurrentItemChanged));
public static bool GetAutoScrollToCurrentItem(DependencyObject obj)
{
return (bool)obj.GetValue(AutoScrollToCurrentItemProperty);
}
public static void OnAutoScrollToCurrentItemChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var listBox = obj as ListBox;
if (listBox == null) return;
var newValue = (bool)e.NewValue;
if (newValue)
listBox.SelectionChanged += listBoxSelectionChanged;
else
listBox.SelectionChanged -= listBoxSelectionChanged;
}
static void listBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var listBox = sender as ListBox;
if (listBox == null || listBox.SelectedItem == null || listBox.Items == null) return;
listBox.Items.MoveCurrentTo(listBox.SelectedItem);
listBox.ScrollIntoView(listBox.SelectedItem);
}
public static void SetAutoScrollToCurrentItem(DependencyObject obj, bool value)
{
obj.SetValue(AutoScrollToCurrentItemProperty, value);
}
}
}
Usage:
<ListBox ItemsSource="{Binding}"
IsSynchronizedWithCurrentItem="True"
behaviors:ScrollCurrentItemIntoViewBehavior.AutoScrollToCurrentItem="True">
If the above code doesn't work for you, give this a try
public class ListBoxExtenders : DependencyObject
{
public static readonly DependencyProperty AutoScrollToCurrentItemProperty = DependencyProperty.RegisterAttached("AutoScrollToCurrentItem", typeof(bool), typeof(ListBoxExtenders), new UIPropertyMetadata(default(bool), OnAutoScrollToCurrentItemChanged));
public static bool GetAutoScrollToCurrentItem(DependencyObject obj)
{
return (bool)obj.GetValue(AutoScrollToSelectedItemProperty);
}
public static void SetAutoScrollToCurrentItem(DependencyObject obj, bool value)
{
obj.SetValue(AutoScrollToSelectedItemProperty, value);
}
public static void OnAutoScrollToCurrentItemChanged(DependencyObject s, DependencyPropertyChangedEventArgs e)
{
var listBox = s as ListBox;
if (listBox != null)
{
var listBoxItems = listBox.Items;
if (listBoxItems != null)
{
var newValue = (bool)e.NewValue;
var autoScrollToCurrentItemWorker = new EventHandler((s1, e2) => OnAutoScrollToCurrentItem(listBox, listBox.Items.CurrentPosition));
if (newValue)
listBoxItems.CurrentChanged += autoScrollToCurrentItemWorker;
else
listBoxItems.CurrentChanged -= autoScrollToCurrentItemWorker;
}
}
}
public static void OnAutoScrollToCurrentItem(ListBox listBox, int index)
{
if (listBox != null && listBox.Items != null && listBox.Items.Count > index && index >= 0)
listBox.ScrollIntoView(listBox.Items[index]);
}
}
Usage in XAML
<ListBox IsSynchronizedWithCurrentItem="True" extenders:ListBoxExtenders.AutoScrollToCurrentItem="True" ..../>

Categories