WPF gets "old" value when data in ListView change - c#

I have the TreeView with objects. When I select item of these tree, than other control - ListView displays as its items properties of selected object. I want to save values of properties when in TreeView selection is change to other object.
So, is there a good way in WPF to gets values of "just before changing" items in ListView control? My idea for now is to override the PreviewMouseDown to check if user click tree node. By god way I mean better than mine. Maybe something in ListView template?
Indication that there is no need to change my idea with the PreviewMouseDown will be also good answer.

Could you please provide the relevant code snippets? I try to answer your question, but I'm not sure I understood it correctly.
If you bind the SelectedItem of you TreeView to a property (a.e. using MVVM pattern), you can save the values before actually setting the item.
Doing so in the setter is not so good though, because it becomes quite large then. I would have a setter like this:
private Foo bar;
public Foo Bar
{
get { return bar; }
set
{
OnPropertyChanging("Bar");
bar=value;
OnPropertyChanged("Bar");
}
}
Then you can listen to your own PropertyChanging events and do your stuff there:
private void this_PropertyChanging(object param, PropertyChangingEventArgs e)
{
switch(e.PropertyName)
{
case "Bar":
//Do you stuff
break,
}
}

Related

Can't deselect ListView Item in MVVM UWP

I want to be able to click ListView item, which then takes me to appropriate page. But since there doesn't exists anything like ClickedItem to go along with the ItemClick, I have to use the SelectedItem (to get the object of what the user clicked) and SelectionChanged to capture when it happens (because this is setup in a way that when user clicks, he makes a selection, which triggers this).
Since in MVVM I can't use events, I'm binding what would be events to methods in my ViewModel.
<GridView x:Name="MyGrid"
ItemsSource="{x:Bind ViewModel.myList, Mode=OneWay}"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
IsSwipeEnabled="false"
SelectedItem="{Binding mySelectedItem, Mode=TwoWay}" // Binding makes it easier to bind the whole object
SelectionChanged="{x:Bind ViewModel.SelectioMade}"
>
I fill up my list in the ViewModel. I'm using Template10 implementation of INotifyPropertyChanged.
private MyListItemClass _mySelectedItem;
public MyListItemClass mySelectedItem{
get { return _mySelectedItem; }
set { Set(ref _mySelectedItem, value); }
}
And this simple method pushes me to the next page when user clickes on an item.
public void SelectioMade() {
if (_mySelectedItem != null) {
NavigationService.Navigate(typeof(Views.DetailPage), _mySelectedItem.id);
}
}
This works.
Problem is that a selection is made and it persists. When I hit the back button on the DetailPage, I go back to this list as I left it and the clicked item is still selected. And hence, clicking it again doesn't actually make a selection and trigger the SelectionChanged.
Obvious choice seemed to be to just set mySelectedItem to null when I no longer need the value, but it doesn't work.
public void SelectioMade() {
if (_mySelectedItem != null) {
NavigationService.Navigate(typeof(Views.DetailPage), _mySelectedItem.id);
mySelectedItem = null;
}
}
I can't seem to be able to set it back to null. If I place a break point on the mySelectedItem = null; it just doesn't do anything. It does trigger the set { Set(ref _mySelectedItem, value); }, but the View doesn't update. Neither the clicked item becomes deselected, nor a TextBlock I bound to one of the mySelectedItem.id properties gets changed (or rather emptied).
I would like to know why doesn't this work and possibly how to fix it. My MVVM may not be perfect, I'm still learning. And while it may not be perfect, I'm not really looking for advice how to properly write MVVM. I want to know why this doesn't work, because in my opinion, it should work just fine.
It seems that GridView doesn't like the SelectedItem property being changed within the SelectionChanged handler (it could result in an infinite loop if guards are not used). You could instead set SelectedItem to null in the OnNavigatedTo handler for that page (or whatever the Template 10 equivalent of that is).
Also you don't really need to subscribe to the SelectionChanged event since you can detect this in the setter of your mySelectedItem property.
However, I think it is wrong to handle item clicks by listening for selection changed events because the selection can be changed by other means (up/down arrow key, or tab key, for example). All you want to do is to respond to an item click and obtain the clicked item, right? For this, you can x:Bind the ItemClick event to a method in your view model:
<GridView ItemClick="{x:Bind ViewModel.ItemClick}" SelectionMode="None" IsItemClickEnabled="True">
public void ItemClick(object sender, ItemClickEventArgs e)
{
var item = e.ClickedItem;
}
If you're uneasy about the ItemClick method signature in your view model, then you can make your own ItemClick behavior to execute a Command exposed in your view model with the command's parameter bound to the clicked item.
If you're not using behaviors for some reason, then you can make your own attached property instead, something like this:
public class ViewHelpers
{
#region ItemClickCommand
public static readonly DependencyProperty ItemClickCommandProperty =
DependencyProperty.RegisterAttached("ItemClickCommand", typeof(ICommand), typeof(ViewHelpers), new PropertyMetadata(null, onItemClickCommandPropertyChanged));
public static void SetItemClickCommand(DependencyObject d, ICommand value)
{
d.SetValue(ItemClickCommandProperty, value);
}
public static ICommand GetItemClickCommand(DependencyObject d)
{
return (ICommand)d.GetValue(ItemClickCommandProperty);
}
static void onItemClickCommandPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var listView = d as ListViewBase;
if (listView == null)
throw new Exception("Dependency object must be a ListViewBase");
listView.ItemClick -= onItemClick;
listView.ItemClick += onItemClick;
}
static void onItemClick(object sender, ItemClickEventArgs e)
{
var listView = sender as ListViewBase;
var command = GetItemClickCommand(listView);
if (command != null && command.CanExecute(e.ClickedItem))
command.Execute(e.ClickedItem);
}
#endregion
}
XAML doesn't require MVVM patterns to be used, which means there is lots of "missing" functionality that you need to write yourself to make MVVM easier for you (like the above ItemClick attached property). Maybe Template 10 provides some behaviors for you already? I'm not familiar with it.
My first instinct would be to check your Set method, to ensure that it is really sending the proper notification to the view. I am not familiar with the Template10 implementation, so it seems strange to me that you are not required to provide a property name with Set().
Beyond that, I would suggest that you go back to using Click rather than SelectionChanged, since that is the behavior you are actually interested in. You should read a bit about attached properties, which are a great way to accomplish tasks that would normally require code-behind without actually using code-behind. They make MVVM a lot more practical and a lot less hackish.
https://msdn.microsoft.com/en-us/library/ms749011(v=vs.110).aspx
An attached property, more or less, allows you to define a DependencyProperty that you can attach to any element in XAML, like your GridView. Because you get access to the Element in the setter, you are free to attach to its events. So, you can create an attached property with a delegate type, which will forward an event like click to the delegate. Back in the view, you bind it to your handler in the ViewModel like this:
<GridView something:MyAttachedProperties.ClickHandler="{Binding MyClickHandler}" />
Hope this helps!
SelectedIndex = -1, following your null set of the SelectedItem property? So yes another property would be required or make sure that caching is disabled for that page as well.

Binding to an array in Xamarin/XAML (implementing breadcrumbs)

I am trying to implement bread crumb navigation. I have a string array in the C# code like this:
public static readonly BindableProperty CurrentPathProperty =
BindableProperty.Create<FileBrowser, string[]>(c => c.CurrentPath, null);
public string[] CurrentPath
{
get { return GetValue(CurrentPathProperty) as string[]; }
set { SetValue(CurrentPathProperty, value); }
}
What would be the correct way to bind to the property in XAML (display the bread crumbs and update CurrentPath when one of them is tapped)? I've tried googling ListViews and x:Array but don't see a straightforward way to do this. I know I need a PropertyChanged event handler but it's not clear to me what needs to happen on the XAML side or what the handler would look like.
If you want your control to update itself when items are added to your array, you should use something implementing INotifyCollectionChanged, like ObservableCollection instead of an array.
With that as source, you can bind it to the ItemSource of a ListView; the ListView being the only Xamarin.Forms controls that supports DataTemplating out of the box.
Anything more complex require defining your own control, inheriting e.g. from StackLayout or any other Layout, and handle creating a Label for each of the string added to your array.

Hair loss and MVVM user controls

I have a user control written in C# & WPF using the MVVM pattern.
All I want to do is have a property in the bound ViewModel exposed to outside of the control. I want to be able to bind to it and I want any changes to the property to be picked up by anything outside the control that is bound to the exposed value.
This sounds simple, but its making me pull out my hair (and there is not much of that left).
I have a dependency property in the user control. The ViewModel has the property implementing the INotifyPropertyChanged interface and is calling the PropertyChanged event correctly.
Some questions:
1) How do I pick up the changes to the ViewModel Property and tie it to the Dependency Property without breaking the MVVM separation? So far the only way I've managed to do this is to assign the ViewModels PropertyChanged Event in the Controls code behind, which is definitely not MVVM.
2) Using the above fudge, I can get the Dependency property to kick off its PropertyChangedCallback, but anything bound to it outside the control does not pick up the change.
There has to be a simple way to do all of this. Note that I've not posted any code here - I'm hoping not to influence the answers with my existing code. Also, you'd probably all laugh at it anyway...
Rob
OK, to clarify - code examples:
usercontrol code behind:
public static DependencyProperty NewRepositoryRunProperty = DependencyProperty.Register("NewRepositoryRun", typeof(int?), typeof(GroupTree),
new FrameworkPropertyMetadata( null, new PropertyChangedCallback(OnNewRepositoryRunChanged)));
public int? NewRepositoryRun
{
get { return (int?)GetValue(NewRepositoryRunProperty); }
set
{
SetValue(NewRepositoryRunProperty, value);
}
}
private static void OnNewRepositoryRunChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.OldValue != e.NewValue)
{
}
}
public GroupTree()
{
InitializeComponent();
GroupTreeVM vm = new GroupTreeVM();
this.DataContext = vm;
}
Viewmodel (GroupTreeVM.cs)
private int? _NewRepositoryRun;
public int? NewRepositoryRun
{
get
{
return _NewRepositoryRun;
}
set
{
_NewRepositoryRun = value;
NotifyPropertyChanged();
}
}
And now for my weekly "don't do that" answer...
Creating a ViewModel for your UserControl is a code smell.
You're experiencing this issue because of that smell, and it should be an indication that you're doing something wrong.
The solution is to ditch the VM built for the UserControl. If it contains business logic, it should be moved to an appropriate location in another ViewModel.
You should think of a UserControl as nothing more than a more complex control. Does the TextBox have its own ViewModel? No. You bind your VM's property to the Text property of the control, and the control shows your text in its UI.
Think of UserControls in MVVM like this--For each model, you have a UserControl, and it is designed to present the data in that model to the user. You can use it anywhere you want to show the user that model. Does it need a button? Expose an ICommand property on your UserControl and let your business logic bind to it. Does your business logic need to know something going on inside? Add a routed event.
Normally, in WPF, if you find yourself asking why it hurts to do something, it's because you shouldn't do it.
Perhaps I've misunderstood, but it seems like you're trying to use binding in the code behind?
public MyUserControl()
{
InitializeComponent();
// Set your datacontext.
var binding = new Binding("SomeVMProperty");
binding.Source = this.DataContext;
SetBinding(MyDependencyProperty, binding);
}

wpf how to refresh bound viewmodel

I'm a newbie to WPF and C# and am building my first app mostly by using code examples. I'm sure there might be some better ways to do this, that I'm not understanding yet, so I'm coming to you guys for some advice.
I have a treeview control with of a bunch of nested objects that is downloaded into an ObservableCollection viewmodel from a WCF Service that I also built. I have the viewmodel declared in the Windows.Resources of the XAML.
My treeview then binds to that StaticResource by its key name.
Items="{Binding Source={StaticResource MyCatalogModel},Path=Items, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
The data in the tree is saved locally to a file. When the viewmodel is instantiated it reads the file, or it creates it if it doesn't exist.
I have 2 related problems right now.
Sometimes the data object that is imported is rather large with lots of nested objects (children). This is taking a long time to update the tree. How can I speed this up? Can I "turn off" the Notify changed stuff of the ObservableCollection, and just reload (rebind?) the viewmodel when it's finished?
I'd like to give the user the ability to basically clear out all the items from the tree and start from scratch. I have code that dumps the underlying file and as I said, it will be recreated when a new viewmodel is instantiated, but I don't know how to "reset" the binding of the resource and the tree. How do I do this?
Thanks to all who respond and any code snippets will be greatly appreciated!!
I had a similar problem, where I had a very large amount of data in a collection - and the event for OnPropertyChanged was firing for each item in the collection. I added an extension with a method to add a range to an ObservableCollection. Here is the code for the extension.
public class SmartCollection<T> : ObservableCollection<T>
{
public SmartCollection()
: base()
{
_suspendCollectionChangeNotification = false;
}
bool _suspendCollectionChangeNotification;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (!_suspendCollectionChangeNotification)
{
base.OnCollectionChanged(e);
}
}
public void SuspendCollectionChangeNotification()
{
_suspendCollectionChangeNotification = true;
}
public void ResumeCollectionChangeNotification()
{
_suspendCollectionChangeNotification = false;
}
public void AddRange(IEnumerable<T> items)
{
this.SuspendCollectionChangeNotification();
int index = base.Count;
try
{
foreach (var i in items)
{
base.InsertItem(base.Count, i);
}
}
finally
{
this.ResumeCollectionChangeNotification();
var arg = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
this.OnCollectionChanged(arg);
}
}
}
So instead of an ObservableCollection its a SmartCollection. What I did was build my collection of objects into a List then you call the AddRange method and pass in your List of objects. This greatly improved the performance.
As far as recreating the tree - if its based on the viewmodel. Just new up the viewmodel that its bound to.
It's amazing how many times the same old questions come up again and again. Right, I have no idea why you are Binding to a StaticResource, but that is a bad idea.
Just create a public property of type ObservableCollection<T> in your view model, set an instance of it as the DataContext of your view in whichever way you prefer or know. Make sure you implement the INotifyPropertyChanged Interface correctly in the code behind, or declare a DependencyProperty instead. Then you can Bind directly to this property, let's call it Items:
<TreeView ItemsSource="{Binding Items}" ... />
When you have it set up this way, then all you need to do to empty or reset the TreeView is this (in the view model):
Items = new ObservableCollection<YourItemDataType>();
As for speed, it's hard to know what you're doing, but WPF is known for being slow when rendering large collections. I can't help with that, sorry.

Selecting a TreeView Item without invoking SelectedItemChanged?

In my app, I have a group of 3d objects and they're exposed to the user through a TreeView. When a user selects an item in the TreeView, an SelectedItemChanged event is fired, the corresponding 3d object is set to be selected and is highlighted in the 3d render window. This works fine.
What I'm having trouble with is the reverse. In a section of my code, I programatically set the selected 3d object in the scene. I want to reflect the currently selected object in the TreeView, so I run through the items until I find the corresponding one. But once I get to it, I can't find a way to make the item appear selected without having SelectedItemChanged being called, which is not what I want.
Is there a way to do this?
Thanks!
I take it you want to suppress the code in your event-handler? If so, a common way of doing this is with a boolean flag (or sometimes an int counter):
bool updatingSelected;
void SomeHandler(object sender, EventArgs args) { // or whatever
if(updatingSelected) return;
//...
}
void SomeCode() {
bool oldFlag = updatingSelected;
updatingSelected = true;
try {
// update the selected item
} finally {
updatingSelected = oldFlag;
}
}
Would it be appropriate to remove the TreeView's SelectedItemChanged event handler temporarily, and re-add it once you've performed the necessary operations? I haven't tried it myself, but it's the only other thing I can think of (Marc Gravell beat me to my original answer - I've done THAT before ;) ).
Good luck!

Categories