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();
}
}
}
Related
Source code is here:
https://github.com/djangojazz/BubbleUpExample
The problem is I am wanting an ObservableCollection of a ViewModel to invoke an update when I update a property of an item in that collection. I can update the data that it is bound to just fine, but the ViewModel that holds the collection is not updating nor is the UI seeing it.
public int Amount
{
get { return _amount; }
set
{
_amount = value;
if (FakeRepo.Instance != null)
{
//The repo updates just fine, I need to somehow bubble this up to the
//collection's source that an item changed on it and do the updates there.
FakeRepo.Instance.UpdateTotals();
OnPropertyChanged("Trans");
}
OnPropertyChanged(nameof(Amount));
}
}
I basically need the member to tell the collection where ever it is called: "Hey I updated you, take notice and tell the parent you are a part of. I am just ignorant of bubble up routines or call backs to achieve this and the limited threads I found were slightly different than what I am doing. I know it could possible be done in many ways but I am having no luck.
In essence I just want to see step three in the picture below without having to click on the column first.
Provided that your underlying items adhere to INotifyPropertyChanged, you can use an observable collection that will bubble up the property changed notification such as the following.
public class ItemObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
public event EventHandler<ItemPropertyChangedEventArgs<T>> ItemPropertyChanged;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
{
base.OnCollectionChanged(args);
if (args.NewItems != null)
foreach (INotifyPropertyChanged item in args.NewItems)
item.PropertyChanged += item_PropertyChanged;
if (args.OldItems != null)
foreach (INotifyPropertyChanged item in args.OldItems)
item.PropertyChanged -= item_PropertyChanged;
}
private void OnItemPropertyChanged(T sender, PropertyChangedEventArgs args)
{
if (ItemPropertyChanged != null)
ItemPropertyChanged(this, new ItemPropertyChangedEventArgs<T>(sender, args.PropertyName));
}
private void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
OnItemPropertyChanged((T)sender, e);
}
}
You should do two things to get it to work:
first: you should refactor the RunningTotal property so it can raise the property changed event. Like so:
private int _runningTotal;
public int RunningTotal
{
get => _runningTotal;
set
{
if (value == _runningTotal)
return;
_runningTotal = value;
OnPropertyChanged(nameof(RunningTotal));
}
}
Second thing you should do is calling the UpdateTotals after you add a DummyTransaction to the Trans. An option could be to refactor the AddToTrans method in the FakeRepo
public void AddToTrans(int id, string desc, int amount)
{
Trans.Add(new DummyTransaction(id, desc, amount));
UpdateTotals();
}
I have a Custom Behavior attached to my ListViewItems that triggers on Loaded and DataContextChanged.
What this behavior does is traverse the VisualTree and by determining its direct parents, set the Visibility to Visible or Collapsed.
On initial load, and whenever I add / remove a ListViewItem to the ListView, it works properly.
However, some interactions only changes the Property of the ViewModel that is binded to the ListViewItem. What I want to do is, whenever this property changes, I want to still trigger the custom behavior that set Visibility for that ListViewItem only. Since the DataContext and Loaded doesn't trigger, my behavior doesn't happen.
Is there a way to do this?
This is my code for reference:
<DataTemplate x:Key="DataTemplate_Item">
<Grid x:Name="Grid_TemplateRoot">
<i:Interaction.Behaviors>
<Dovetail_UI_Register_Controls_Behaviors:SetItemVisibilityBehavior />
</i:Interaction.Behaviors>
<TextBlock Text={Binding Path="ItemName"}
</Grid>
</DataTemplate>
And the behavior:
public class OnLoadedOrDatacontextChangedBehavior<T> : OnLoadedBehavior<T> where T : FrameworkElement
{
protected override void OnAttached()
{
base.OnAttached();
TypedAssociatedObject.Loaded += ChangeVisibility();
TypedAssociatedObject.AddDataContextChangedHandler(OnDataContextChanged);
}
protected override void OnDetaching()
{
base.OnDetaching();
TypedAssociatedObject.Loaded -= ChangeVisibility();
TypedAssociatedObject.RemoveDataContextChangedHandler(OnDataContextChanged);
}
protected virtual void OnDataContextChanged(object sender, EventArgs args)
{
ChangeVisibility();
}
private void ChangeVisibility()
{
//Change visibility here
}
}
Thank you!
If you're not publishing this as a class library that needs to support data context coming in as any object whatsoever, then you can update your handler to listen for those property changes on your data context.
For example:
protected virtual void OnDataContextChanged(object sender, EventArgs args)
{
ChangeVisibility();
// Listen for any further changes which effect visibility.
INotifyPropertyChanged context = ((FrameworkElement)sender).DataContext as INotifyPropertyChanged;
context.PropertyChanged += (s, e) => ChangeVisibility();
}
You could additionally extend this further, for example if your handler methods for data context changing use DependencyPropertyChangedEventHandler, then you could cleanup that PropertyChanged handler. Also, you can watch for only specific properties in your PropertyChanged handler. Expanded example:
protected virtual void OnDataContextChanged(object sender, DependencyPropertyChangedEventHandler args)
{
ChangeVisibility();
INotifyPropertyChanged context;
// Cleanup any handler attached to a previous data context object.
context = e.OldValue as INotifyPropertyChanged;
if (context != null)
context.PropertyChanged -= DataContext_PropertyChanged;
// Listen for any further changes which effect visibility.
context = e.NewValue as INotifyPropertyChanged;
if (context != null)
context.PropertyChanged += DataContext_PropertyChanged;
}
private void DataContext_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "MyTargetProperty")
{
ChangeVisibility();
}
}
How to use GotFocus() and LostFocus() from ViewModel?
private void TxtDescribeGroup_GotFocus(object sender, RoutedEventArgs e)
{
TxtDescribeGroup.BorderBrush = new SolidColorBrush(Colors.Orange);
}
private void TxtDescribeGroup_LostFocus(object sender, RoutedEventArgs e)
{
TxtDescribeGroup.BorderBrush = new SolidColorBrush(Colors.Gray);
}
This code written in the Xaml.CS.
But I want to write all the code in ViewModel.
Any one let me know how to write the events in ViewModel?
And also how to write the selection changed event in ViewModel for ListBox?
private void lstShow_Tap(object sender, GestureEventArgs e)
{
if (lstShow.SelectedItem != null)
{
ListBox item = (ListBox)sender;
LoginPageModel listItem = (LoginPageModel)item.SelectedItem;
MessageBox.Show("Selected FirstName==> " + listItem.FirstName);
}
}
This is also written in Xaml.Cs. How to write in the ViewModel.
Thanks in advance..
In XAML (assuming you already set your DataContext)
<Border BorderBrush="{Binding Path=BorderBrush}">
... your stuff here
</Border>
Then in your ViewModel (assuming you implement INotifyPropertyChanged) just add a property:
private Brush borderBrush;
public Brush BorderBrush {
get { return borderBrush; }
set {
if(value!=borderBrush) {
value=borderBrush;
// this notifies your UI that the property has changed and it should read the new value
// should be already declared in your view model or base view model or whatever
// MVVM framework you are using
OnPropertyChanged("BorderBrush");
}
}
}
I'm using a TrulyObservableCollection as a datasource in a WPF DataGrid. My class implements the PropertyChange event properly (I get notification when a property changes). The CollectionChanged event gets triggered as well. However, my issue lies in the connection between the PropertyChanged event and CollectionChanged event. I can see in the PropertyChanged event which item is being changed (in this case the sender object), however I can't seem to find a way to see which one is changed from within the CollectionChanged event. The sender object is the whole collection. What's the best way to see which item has changed in the CollectionChanged event? The relevant code snippets are below. Thank you for your help, and let me know if there needs to be some clarification.
Code for setting up the collection:
private void populateBret()
{
bretList = new TrulyObservableCollection<BestServiceLibrary.bretItem>(BestClass.BestService.getBretList().ToList());
bretList.CollectionChanged += bretList_CollectionChanged;
dgBretList.ItemsSource = bretList;
dgBretList.Items.Refresh();
}
void bretList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
//Do stuff here with the specific item that has changed
}
Class that is used in the collection:
public class bretItem : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private int _blID;
public string _blGroup;
[DataMember]
public int blID
{
get { return _blID; }
set
{
_blID = value;
OnPropertyChanged("blID");
}
}
[DataMember]
public string blGroup
{
get { return _blGroup; }
set
{
_blGroup = value;
OnPropertyChanged("blGroup");
}
}
protected void OnPropertyChanged (String name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
TrulyObservableCollection class
public class TrulyObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
public TrulyObservableCollection()
: base()
{
CollectionChanged += new NotifyCollectionChangedEventHandler(TrulyObservableCollection_CollectionChanged);
}
public TrulyObservableCollection(List<T> list)
: base(list)
{
foreach (var item in list)
{
item.PropertyChanged += new PropertyChangedEventHandler(item_PropertyChanged);
}
CollectionChanged += new NotifyCollectionChangedEventHandler(TrulyObservableCollection_CollectionChanged);
}
void TrulyObservableCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (Object item in e.NewItems)
{
(item as INotifyPropertyChanged).PropertyChanged += new PropertyChangedEventHandler(item_PropertyChanged);
}
}
if (e.OldItems != null)
{
foreach (Object item in e.OldItems)
{
(item as INotifyPropertyChanged).PropertyChanged -= new PropertyChangedEventHandler(item_PropertyChanged);
}
}
}
void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
NotifyCollectionChangedEventArgs a = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
OnCollectionChanged(a);
}
}
EDIT:
In the item_PropertyChanged event the NotifyCollectionChangedEventArgs are set with NotifyCollectionChangedAction.Reset. This causes the OldItems and NewItems to be null, therefore I can't get the changed item in that case. I can't use .Add as the Datagrid is updated with an additional item. I can't appear to get .Replace to work either to get the changed item.
How about this:
In your ViewModel that contains the ObservableCollection of bretItem, the ViewModel subscribes to the CollectionChanged event of the ObservableCollection.
This will prevent the need of a new class TrulyObservableCollection derived from ObservableCollection that is coupled to the items within its collection.
Within the handler in your ViewModel, you can add and remove the PropertyChanged event handler as you are now. Since it is now your ViewModel that is being informed of the changes to objects within the collection, you can take the appropriate action.
public class BretListViewModel
{
private void populateBret()
{
bretList = new ObservableCollection<BestServiceLibrary.bretItem>(BestClass.BestService.getBretList().ToList());
bretList.CollectionChanged += bretList_CollectionChanged;
}
void bretList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (Object item in e.NewItems)
{
(item as INotifyPropertyChanged).PropertyChanged += new PropertyChangedEventHandler(item_PropertyChanged);
}
}
if (e.OldItems != null)
{
foreach (Object item in e.OldItems)
{
(item as INotifyPropertyChanged).PropertyChanged -= new PropertyChangedEventHandler(item_PropertyChanged);
}
}
}
void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
var bret = sender as bretItem;
//Update the database now!
//One note:
//The ObservableCollection raises its change event as each item changes.
//You should consider a method of batching the changes (probably using an ICommand)
}
}
A Thing of Note:
As an aside, it looks like you are breaking the MVVM pattern based upon this snippet:
dgBretList.ItemsSource = bretList;
dgBretList.Items.Refresh();
You probably should consider loading your ViewModel and binding your View to it instead of coding logic in the code-behind of your View.
It's not appropriate to use the collection changed event in this way because it's only meant to be fired when adding/removing items from the collection. Which is why you've hit a wall. You're also in danger of breaking the Liskov substitution principle with this approach.
It's probably better to implement the INotifyPropertyChanged interface on your collection class and fire that event when one of your items fires its property changed event.
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