Navigation Back & MVVM - How to refresh WP8 page databinding - c#

I'm doing a WP8 App (C#/XAML).
In my view I specify a button, which contect is set by binding with a callback, for the start of an app, when the VM is not fully loaded.
MVVM looks like:
ViewModel
---------
+ Model
-----
+Property
And is created in App.xaml.cs like this:
public static MainViewModel ViewModel
{
get
{
if (viewModel == null)
{
viewModel = new MainViewModel();
}
return viewModel;
}
}
And set to the page as datacontext in contructor of the page:
DataContext = App.ViewModel;
And button:
<Button x:Name="btn" Content="{Binding Model.Property, FallBackValue='click to load'}" .../>
At the start, the btn does not have a value to put in it's content, because model is empty.
When the btn is clicked, it loads the model. It fills model with data and navigates to another page which shows that data.
And when i navigate back (through hardware back button) I'd like btn to use the value from binding instead of a fallback, because the value is already set. But It doesn't use it and still uses the one provided by FallbackValue argument of binding.
How to ensure, that the page "refreshes" an uses actual values provided by ViewModel?

Ahh, ok found solution to my problem myself.
THE PROBLEM
If you're using static Datacontext (if the Viewmodel class you use is created as static), then when you navigate back to the page, the databinding won't update (at least that is how it was in my case).
I use same datacontext (ViewModel containing multiple models and inside some collections and properties) for multiple pages. But when I navigated back to the page through hardware back button, the databinding was not updated.
The content of a button/textblock is stuck at the old value, even though you changed it to some new one.
Solution
Override the OnNavigatedTo Method, and set databinding there instead in contructor. This way you can be sure, that the databinding is always "fresh" and updated.
Inside the page class in code-behind (the .xaml.cs file sticked to your .xaml page) write this:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e); //can be left out, base method is empty
DataContext = null; //setting datacontext empty at first
DataContext = App.ViewModel; //and setting it to the static ViewModel i created
}
This way, the DataContext is always first set to null, when I come to the page (so that the old values clean and there is nothing to bind from).
And shortly after that, i put the original DataContext back, so it has something to bind from again.
The step with null is necessary, because i need the datacontext property to change, otherwise if I just point again at the same obect that is already set as dataContext, nothing will happen.

I guess your ViewModel would be implementing INotifyPropertyChanged. To refresh the data binding you just need to raise property change event implemented in your model. In OnNavigatedTo event of your page check if Model is empty or not. If not raise property change
In your view model
public class ViewModel:INotifyPropertyChanged
{
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
In your page
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
if (App.ViewModel != null)
App.ViewModel.NotifyPropertyChanged("Name of property");
}

Related

Multiple views sharing same data with two-way data binding between multiple threads

UWP app ( mvvm architecture ) I have a MainView which has a collection in its ViewModel, used to bind to the GridView on MainView and each item has a TextBox with 2 way databinding with Description property of class Note.
Xaml of the TextBox of each gridviewitem.
<TextBlock Text="{x:Bind Description,Mode=TwoWay}"
Collection property used to bind to ItemSource of gridview.
public ObservableCollection<Note> Notes { get; }
and this is the class Note
public class Note : Observable
{
private string _description;
public string Description
{
get => _description;
set => Set(ref _description, value, nameof(Description));
}
}
the Observable class is for two way data binding help.
public class Observable : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void Set<T>(ref T storage, T value, [CallerMemberName]string propertyName = null)
{
if (Equals(storage, value))
{
return;
}
storage = value;
OnPropertyChanged(propertyName);
}
protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Now everything uptil this point works perfectly, when I change the text in textbox, it changes the value of Description as well.
Second View
Now I have a feature where each GridViewItem has a button in it which opens the Note in new window. and this new window has nothing but only 1 TextBox, so now the secondary view and the GridViewItem which opened that view are using the same object of Note.
This TextBox in secondary view also has 2 way data binding with the Description of the Note.
The Problem
What I want is that whether the textbox in gridview or the textbox on the secondary view is edited, the value of description must remain synced between these 2 textboxes, that is why I tried to bind them 2 way with same object of Note hence the same Description object is bound to both of them.
Error here was expected to me which was Marshalling threading error, so whenever I try to change value of any textbox, it tried to update UI on other view ( which is another thread ) which is ofcourse not allowed.
I know about CoreDisptcher
I already know about the Dispatcher feature of UWP for safe cross thread communication, I already have it all setup and if I use it from a normal method I can easily use it for cross thread UI update and it totally works. But my issue is the following line :
protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));\
Exception occurs when it tried to invoke the PropertyChanged I tried to wrap following line in my Dispatcher :
OnPropertyChanged(propertyName);
but INotify interface does not allow me to have a Set<> method which returns a Task instead it needs to return just an object, this is the point where I am stuck and I dont know how to make use of Dispatcher in this scenario, please let me know if there is some better way to do this, it seems this way might not be so efficient.
Thanks.
The best solution in this case would be to have a separate set of INotifyPropertyChanged instances for each window and using some kind of messaging solution like EventHub in MvvmLight, which publishes message that the underlying model changed and all interested parties should update their instances.
Another option would be to create a base model class, which maintains a dictionary of INotifyPropertyChanged instances for each UI thread (so it would be a Dictionary<Dispatcher, YourModelClass>. Now the parent would subscribe to PropertyChanged event of each child instance and once it executes would propagate the event to other childs using the appropriate Dispatcher.
Also there is a very interesting utility class ViewSpecificBindableClass by Marian Dolinský on his GitHub which could potentially be a solution that would allow you to have "single" class in multiple views, aware of multiple dispatchers. I haven't tried it yet, but it seems promising.
So I finally had to take a totally different approach centralizing TextChanged events of MainView textbox and the one on the secondaryview.
I essentially passed the textbox on the mainpage through to the secondary page ( secondary view ) and then subscribed to its TextChanged event. I also subscribed to the TextChanged event of textbox on the secondary view, and then with help of reverse dispatchers I was able to sync the text between 2 windows without any problems.
Note : always make sure to unsubscribe to events when the secondary window closes to prevent memory leaks.
private async void PipBox_TextChanged(object sender, TextChangedEventArgs e)
{
string text = PipBox.Text;
await CoreApplication.MainView.Dispatcher.AwaitableRunAsync(() =>
{
if (parentBox.Text != text)
parentBox.Text = text;
});
}
private async void ParentBox_TextChanged(object sender, TextChangedEventArgs e)
{
string text = parentBox.Text;
// the awaitablerunasync extension method comes from "Windows Community Toolkit".
await _viewLifetimeControl.Dispatcher.AwaitableRunAsync(() =>
{
if (ViewModel.MyNote.Description != text)
ViewModel.MyNote.Description = text;
});
}
Notice that I still have 2 way data binding on both textboxes and it does not cause any exceptions because I am using 2 different instances of Note for both views.
<TextBox Text="{x:Bind ViewModel.MyNote.Description, Mode=TwoWay}"
x:Name="PipBox"/>
but because I have twoway data binding on both textboxes, that is how I can easily keep both instances of Note in sync as well on separate threads.
I will keep the github repo in case it can help anyone else : https://github.com/touseefbsb/MultiWindowBindingSync
P.S : A special thanks to Martin Zikmund who helped me a lot in figuring out this solution.

XAML x:bind oneTime on initially null property still works. Why?

x:Bind defaults to OneTime, which updates the target UI with the data when the Page's Loading event triggers the generated code's Initialize function.
I have a Page with a ViewModel property. This ViewModel class implements INPC for its properties. The data for the viewModel is loaded asynchronously, only after the page is loaded. So on Page initialization, and subsequently the generated code initialization, the UI target using x:Bind will have null data.
Since it is OneTime, it shouldn't change unless I manually call Update(which I don't).
So why does my x:Bind UI work?
The following is some simplified code snippets.
<Page x:Name="MyPage" x:Class="MyProject.Pages.MyPage">
<Button Command="{x:Bind ViewModel.GoToAnotherPageCommand}">
public sealed partial class MyPage : Page
{
public MyPageViewModel ViewModel { get; set; }
public MyPage()
{
this.InitializeComponent();
}
// called by an event bound to a Frame's Navigated, which all pages use
public void OnNavigatedTo()
{
this.ViewModel = new MyPageViewModel();
}
}
public class MyPageViewModel : INotifyPropertyChanged, INotifyPropertyChanging
{
// GoToAnotherPageCommand is an INPC property and its set in the constructor
The reason that your command works fine is because OnNavigatedTo will be called before the command instantiation. This means by the time the code tries to set the command, the ViewModel has already been instantiated and is no longer null.
To prove my point, first go open the file under the following path(could be ARM or *x64 depending on which platform you are running on) -
obj/x86/Debug/MyPage.g.cs
This is basically the code-generated file that hooks up all the x:Bind stuff for your page.
Now put a breakpoint at where the command is set. In my case, it's a method called Set_Windows_UI_Xaml_Controls_Primitives_ButtonBase_Command. Then put another breakpoint at OnNavigatedTo.
Now run the app, you will see that the OnNavigatedTo method gets called first.
If your page's NavigationCacheMode is set to Disabled, this behavior makes OnNavigatedTo the ideal place to instantiate x:Bind bindings so the page only uses memory to create these new objects when the user actually navigates to it, instead of doing everything inside the page constructor.
Don't do this inside the Loaded event of the Page though. Because it will get called after the command instantiation. You can try the following code to instantiate the ViewModel, and the result is very different(your command will not work).
public MyPage()
{
InitializeComponent();
Loaded += (s, e) => ViewModel = new MyPageViewModel();
}
The compiled binding system (x:Bind) is smart enough to check for initial null values and not consider them the actual value you wish to bind. It will wait for the first non-null value and bind that value.
This is by design, as binding to an initial null value is (almost) never the intention of the binding.
I didn't find the source of this information, but I believe it was in the Build talk detailing the x:Bind system in 2015.
Updated:
As Justin mentions in the comments below and in his own answer, the binding will not work if the view model is set after the binding operation happens.
I believe this is because the binding terminates when it encounter a null reference in the property chain, but I haven't tested this, so I might be incorrect.

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.

Refresh all contexts on SaveChanges

I have a WPF application where I have two windows. One is for listing objects (e.g. Books) and the other one is a form for editing one object (e.g. a Book). I create a new EF context for each of the windows.
Now I wonder, is there a way I can detect in the list window that a context has been edited after I call SaveChanges in the edit window?
What I do at the moment is to have a public Update method on my list window object that I call from the edit window after SaveChanges is called and in that function I call for the context refresh. However that is not scalable as I would need to keep track of all the windows that depend on Book information (e.g. if I add Shelves list I would need to make sure to update that one as well).
Any suggestions on how to solve this in more modular fashion?
P.S.
The funny part is that I seemed to have had an idea of how to improve this 2 years ago when I first wrote this code as I made a comment for myself: "TODO: solve this differently by having events in ArTresorEntities". But I don't remember what I meant.
Using MVMM Light you can send Message. Your List Window registers receiving message and latter notifies it about change. Take a look here.
You can do this by implementing INotifyPropertyChanged on your model class and then binding the value to your label/interface control.
Here's an example model class:
public class Data : INotifyPropertyChanged
{
int random;
public int Random
{
get { return random; }
set { random = value; NotifyPropertyChanged("Random"); }
}
protected void NotifyPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
That's all that's needed to implemented the INotifyPropertyChanged. Notice that you have to call NotifyPropertyChanged during the set method of the property, otherwise this won't work.
On the UI side, you would do something like this:
<Grid>
<Label Content="{Binding Random, UpdateSourceTrigger=PropertyChanged}" />
<Button Width="100" Height="40" Content="Randomize" Click="Button_Click" />
</Grid>
And then lastly, the code-behind for the xaml:
public Data data;
public MainWindow()
{
InitializeComponent();
data = new Data();
this.DataContext = data;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
data.Random = new Random().Next(1000);
}
The DataContext being assigned is just an example, but it binds this particular class to this window's dataContext, allowing me to do the {Binding Random} and know which property I'm talking about and in which class. You can do this in a few ways and split it up throughout a view.
I also added a button that generates a random number and sets it in my Random property on my model. If you run this code, it updates the label each time you click the button.

MVVM: Bring control into view

I have a Grid with a ScrollViewer around it. At the top of my ScrollViewer is a Button. On a Click on the Button, I want the ScrollViewer to scroll to a Control at the bottom of the ScrollViewer.
With the following XAML I can bring the Control into view:
<Button Grid.Row="2" Content="Some Button" Command="{Binding DoJumpCommand}" CommandParameter="{Binding ElementName=window}"/>
The Command in the ViewModel is:
if (parameter is MainWindowView)
{
var mainWindowView = parameter as MainWindowView;
mainWindowView.myJumpTarget.BringIntoView();
}
This works fine. But I'm not sure if this is clean MVVM because I pass the complete View into the ViewModel.
Is there a better way to do this?
When I first saw your question, I thought that the general solution to handling events with MVVM is to handle them in an Attached Property. However, looking again, it occurred to me that you're not actually handling any events... you just want to call a method from a UI control. So really, all you need is a way to pass a message from the view model to the view. There are many ways to do this, but my favourite way is to define a custom delegate.
First, let's create the actual delegate in the view model:
public delegate void TypeOfDelegate();
It doesn't need any input parameters, because you don't need to pass anything from the view model to the view, except a signal... your intention to scroll the ScrollViewer.
Now let's add a getter and setter:
public TypeOfDelegate DelegateProperty { get; set; }
Now let's create a method in the code behind that matches the in and out parameters of the delegate (none in your case):
public void CanBeCalledAnythingButMustMatchTheDelegateSignature()
{
if (window is MainWindowView) // Set whatever conditions you want here
{
window.myJumpTarget.BringIntoView();
}
}
Now we can set this method as one (of many) handlers for this delegate in a Loaded event handler in the view code behind:
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// Assumes your DataContext is correctly set to an instance of YourViewModel
YourViewModel viewModel = (YourViewModel)DataContext;
viewModel.DelegateProperty += CanBeCalledAnythingButMustMatchTheDelegateSignature;
}
Finally, let's call our delegate from the view model... this is equivalent to raising the event:
if (DelegateProperty != null) DelegateProperty(dataInstanceOfTypeYourDataType);
Note the important check for null. If the DelegateProperty is not null, then all of the attached handler methods will be called one by one. So that's it! If you want more or less parameters, just add or remove them from the delegate declaration and the handling method... simple.
So this is an MVVM way to call methods on a UI control from a view model. However, in your case it could well be argued that implementing this method would be overkill, because you could just put the BringIntoView code into a basic Click handler attached to your Button. I have supplied this answer more as a resource for future users searching for a way to actually call a UI method from a view model, but if you also chose to use it, then great!

Categories