Intercepting property changes with CommunityToolKit.Mvvm amd Xamarin Forms - xamarin

I've been moving code from mvvmlight to the CommunityToolkit.Mvvm framework with my Xamarin Forms project and have hit a snag.
In mvvmlight, I would have a property like this
bool loginOK;
public bool LoginOK
{
get => loginOK;
set => Set(()=>LoginOK, ref loginOK, value, true);
}
in CommunityToolkit.Mvvm, this becomes
bool loginOK;
public bool LoginOK
{
get => loginOK;
set => SetProperty(ref loginOK, value);
}
Accorrding to the docs, if the property changes, the PropertyChanging event is fired
In my code behind in (in Xam.Forms), I have this
protected override void OnAppearing()
{
base.OnAppearing();
ViewModel.PropertyChanged += ObservableObject_PropertyChanged;
}
async void ObservableObject_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "LoginOK":
if (ViewModel.LoginOK)
{
if (ViewModel.SkipWelcome)
{
var d1 = await image.ScaleTo(4, 500);
if (!d1)
{
Device.StartTimer(TimeSpan.FromMilliseconds(500), () =>
{
Device.BeginInvokeOnMainThread(async () => await Shell.Current.GoToAsync("//TabBar"));
return false;
});
}
}
}
else
{
var d2 = await image.ScaleTo(8, 500);
if (!d2)
{
var d3 = await image.ScaleTo(0, 500);
if (!d3)
{
Device.StartTimer(TimeSpan.FromMilliseconds(500), () =>
{
Device.BeginInvokeOnMainThread(async () => await Shell.Current.GoToAsync("//Welcome"));
return false;
});
}
}
}
break;
}
}
When I run this and set a break point on the line
var d2 = await image.ScaleTo(8,500);
The break is not hit, but the imge shows
Am I doing something wrong to intercept the property changing or is there something I'm missing from the docs?

The main issue you are seeing is because loginOK defaults to false and when a login attempt fails LoginOK does not technically change and therefore SetProperty does not result in raising the PropertyChanged event. If you look at the codebase for the toolkit you will notice that they always check whether the value has changed before raising any notifications.
There are a number of options that you could consider to work around this problem:
1 - switch to bool?
If you switch to using a nullable bool then it's value will default to null and therefore when a login attempt fails false will cause PropertyChanged to fire.
2 - use an enum
Similar to the bool? approach in point 1 but instead use something like a LoginResult enum like the following:
public enum LoginResult
{
None = 0,
Failed,
Success
}
This will make you code much more readable than point 1.
3 - fire a specific event
Rather than rely on PropertyChanged you could create your own event (e.g. LoginAttempted) you could then keep your bool indicating success or not if you so desired.
4 - make use of a Command
Events are sometimes consider a bad concept of the past so you could instead create an ICommand property on your View and assign it to your View Model allowing the View Model to call Execute on it and pass the log result up to the view. Something like:
View
public ViewConstructor()
{
ViewModel.LoginAttemptCommand = new RelayCommand<bool>(() => OnLoginAttempted());
}
public void OnLoginAttempted(bool result)
{
// Old logic on whether login succeeded.
}
ViewModel
public ICommand LoginAttemptCommand { get; set; }
// Wherever you set LoginOK = false/true call:
LoginAttemptCommand.Execute(false); // or true if it succeeds.

Related

Update listview from API call in certain interval- xamarin.forms

I am developing a chat application in xamarin.forms.The viewmodel binded to my chat listview page have an API call , which will fetch the chat data and bind to the listview.The API will call only once ie; when we open the page. What I am trying to do is call the API every 10 seconds and update the listview if there are new messages.But what happening is instead of updating the list, it duplicates the entire data.I think it is normal that if the API called again, it will rebind the entire data. How can I make this update the listview if any new message available? like a chat APP works.Any help or guidance is appreciated.
The API data will be assigned to incoming and outgoing cell according to a parameter.
My viewmodel;
public class ChatPageViewModel : INotifyPropertyChanged
{
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public INavigation Navigation { get; set; }
public string APropertyToSet { get; set; }
public ObservableCollection<NCMessage> Messages { get; set; } = new ObservableCollection<NCMessage>();
public ObservableCollection<ChatData> ChatListObj { get; set; }
public ChatPageViewModel(INavigation navigation)
{
// This is how I call the timer
Device.StartTimer(TimeSpan.FromSeconds(10), () =>
{
Device.BeginInvokeOnMainThread(async () =>
{
await loadChatList();
});
return true;
});
// <--------------- Load chat List API-------------------->
async Task loadChatList()
{
await Task.Run(async () =>
{
try
{
// API call is the dedicated class for makin API call
APICall callForNotificationList = new APICall("apicallUrl/CallChatList", null, null, "GET");
try
{
ChatListObj = callForNotificationList.APICallResult<ObservableCollection<ChatData>>();
if (ChatListObj[0].results.Count != null && ChatListObj[0].results.Count != 0)
{
if (ChatListObj[0].success)
{
foreach (var item in ChatListObj[0].results)
{
if (item.type == "user")
{
if (!string.IsNullOrEmpty(item.message))
{
var message = new NCMessage
{
Text = item.message.ToString(),
IsIncoming = "True"
};
Messages.Add(message);
}
}
}
}
else
{
//error message
}
}
else
{
//error message
}
}
catch (Exception e)
{
}
}
catch (Exception ex)
{
}
});
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
My chat XAML
<ListView
ItemTemplate="{StaticResource MessageTemplateSelector}"
ItemsSource="{Binding Messages,Mode=OneWay}"
Margin="0"
BackgroundColor="Transparent"
SelectionMode="None"
FlowDirection="RightToLeft"
HasUnevenRows="True" x:Name="ChatList"
VerticalOptions="FillAndExpand"
SeparatorColor="Transparent"
>
</ListView>
My XAML.cs
public partial class ChatPage : ContentPage
{
ChatPageViewModel vm;
public ChatPage()
{
InitializeComponent();
this.BindingContext = vm = new ChatPageViewModel(Navigation);
}
protected async override void OnAppearing()
{
base.OnAppearing();
await Task.Delay(2000);
await vm.loadChatList();
}
}
you are doing this for every message you retrieve from your API without checking to see if the message is already in the collection
Messages.Add(message);
you have (at least) three options
clear the entire Messages collection before calling the API
check if a message exists before adding it to the collection
modify the API call to only return new messages
The usual chat systems work using Socket connections, so that server can push just the new messages that it has received, as opposed to your current pull based system. Take a look at SignalR if you want to pursue that path, here's a good example - Real Time Chat App
As for your current code, there are a few possibilities:
As #Jason has mentioned, you could clear out the existing messages and add all of them again, you should use RangeObservableCollection so that it doesn't redraw the list every time you add a message
You could maintain a Id for all the chat messages and send the Id in your API call, ideally filtering should happen the server side, no point in extra load on the client.
new APICall("apicallUrl/CallChatList?from_id=<last_message_id>", null, null, "GET")
If there are no last_message_id, send all the data. This eliminates unnecessary data transfer, as you'll send only 1 new message than the 10k previous messages :)

Xamarin.Forms Calling Dialog from Xamarin.Android Project Throws an Exception

I've been trying to call a Dialog Alert from my xamarin.forms app using IoC container so far I haven't been able to display an alert in my app using this code:
Code at my viewModel:
await _dialogService.DisplayMessageAsync("ERROR", "There are errors on your form!", "Cancel", null);
at my shared project I have this DialogService which is the one I call from my viewModel:
public class DialogService : IDialogService
{
readonly IDialogService _dialogService;
public DialogService()
{
_dialogService = DependencyService.Get<IDialogService>();
}
public void CloseAllDialogs()
{
_dialogService.CloseAllDialogs();
}
public async Task DisplayMessageAsync(string title, string message, string buttonCancelName, Action callback)
{
await _dialogService.DisplayMessageAsync(title, message, buttonCancelName, callback);
}
public async Task DisplayMessageConfirmAsync(string title, string message, string buttonOkName, string buttonCancelName, Action<bool> callback)
{
await _dialogService.DisplayMessageConfirmAsync(title, message, buttonOkName, buttonCancelName, callback);
}
}
so at my Xamarin.Android.XXXXX I have the implementation of my DialogService which is call from my DialogService at my Shared Project this is the code:
public class DialogService : IDialogService
{
List<AlertDialog> _openDialogs = new List<AlertDialog>();
public void CloseAllDialogs()
{
foreach (var dialog in _openDialogs)
{
dialog.Dismiss();
}
_openDialogs.Clear();
}
public async Task DisplayMessageAsync(string title, string message, string okButton, Action callback)
{
await Task.Run(() => Alert(title, message, okButton, callback));
}
public async Task DisplayMessageConfirmAsync(string title, string message, string okButton, string cancelButton, Action<bool> callback)
{
await Task.Run(() => AlertConfirm(title, message, okButton, cancelButton, callback));
}
bool Alert(string title, string content, string okButton, Action callback)
{
var activity = (Activity)Forms.Context;
//var activity = (Activity)Android.App.Application.Context;
var alert = new AlertDialog.Builder(Android.App.Application.Context);
//var alert = new AlertDialog.Builder(activity);
alert.SetTitle(title);
alert.SetMessage(content);
alert.SetNegativeButton(okButton, (sender, e) =>
{
if (!Equals(callback, null))
{
callback();
}
_openDialogs.Remove((AlertDialog)sender);
});
Device.BeginInvokeOnMainThread(() =>
{
AlertDialog dialog = alert.Show();
_openDialogs.Add(dialog);
dialog.SetCanceledOnTouchOutside(false);
dialog.SetCancelable(false);
});
return true;
}
bool AlertConfirm(string title, string content, string okButton, string cancelButton, Action<bool> callback)
{
var alert = new AlertDialog.Builder(Android.App.Application.Context);
alert.SetTitle(title);
alert.SetMessage(content);
alert.SetNegativeButton(cancelButton, (sender, e) =>
{
callback(false);
_openDialogs.Remove((AlertDialog)sender);
});
alert.SetPositiveButton(okButton, (sender, e) =>
{
callback(true);
_openDialogs.Remove((AlertDialog)sender);
});
Device.BeginInvokeOnMainThread(() =>
{
var dialog = alert.Show();
_openDialogs.Add(dialog);
dialog.SetCanceledOnTouchOutside(false);
dialog.SetCancelable(false);
});
return true;
}
}
so whenever the private method alert is called it throws an Exception like this:
Unhandled Exception:
Android.Views.WindowManagerBadTokenException: Unable to add window --
token null is not valid; is your activity running?
it can be corrected if I switch this line of code:
var alert = new AlertDialog.Builder(Android.App.Application.Context);
for this line of code:
var activity = (Activity)Forms.Context;
var alert = new AlertDialog.Builder(activity);
the problem of using this I get a Xamarin.Forms warning like this:
'Forms.Context' is obsolete: 'Context is obsolete as of version 2.5.
Please use a local context instead.'
and i'm a bit obsesive I dont like to have warning and try to maintain my code as updated as possible so, can somebody help me make this code work without needing to use obsolete code. because I found answers that just replacing (Activity)Forms.Context for Android.App.Application.Context would work, but in this case it isn't working at all.
hopefully someone can point me in the right direction because i haven't been able to find any documentation about this case specifically.
Problem is that Android.App.Application.Context is not always an Activity Context.
Xamarin removed this and added a new constructor for the Renderers which include the context. The problem comes in cases like this where you are working on something that is not a CustomRenderer.
For these cases, I use James' Plugin CurrentActivityPlugin which will keep track of the current activity for you. Find it here
Hope this helps.-

How to handle/cancel back navigation in Xamarin Forms

I tried to use the back navigation by overriding OnBackButtonPressed, but somehow it wasn't get called at all. I am using the ContentPage and the latest 1.4.2 release.
Alright, after many hours I figured this one out. There are three parts to it.
#1 Handling the hardware back button on android. This one is easy, override OnBackButtonPressed. Remember, this is for a hardware back button and android only. It will not handle the navigation bar back button. As you can see, I was trying to back through a browser before backing out of the page, but you can put whatever logic you need in.
protected override bool OnBackButtonPressed()
{
if (_browser.CanGoBack)
{
_browser.GoBack();
return true;
}
else
{
//await Navigation.PopAsync(true);
base.OnBackButtonPressed();
return true;
}
}
#2 iOS navigation back button. This one was really tricky, if you look around the web you'll find a couple examples of replacing the back button with a new custom button, but it's almost impossible to get it to look like your other pages. In this case I made a transparent button that sits on top of the normal button.
[assembly: ExportRenderer(typeof(MyAdvantagePage), typeof
(MyAdvantagePageRenderer))]
namespace Advantage.MyAdvantage.MobileApp.iOS.Renderers
{
public class MyAdvantagePageRenderer : Xamarin.Forms.Platform.iOS.PageRenderer
{
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
if (((MyAdvantagePage)Element).EnableBackButtonOverride)
{
SetCustomBackButton();
}
}
private void SetCustomBackButton()
{
UIButton btn = new UIButton();
btn.Frame = new CGRect(0, 0, 50, 40);
btn.BackgroundColor = UIColor.Clear;
btn.TouchDown += (sender, e) =>
{
// Whatever your custom back button click handling
if (((MyAdvantagePage)Element)?.
CustomBackButtonAction != null)
{
((MyAdvantagePage)Element)?.
CustomBackButtonAction.Invoke();
}
};
NavigationController.NavigationBar.AddSubview(btn);
}
}
}
Android, is tricky. In older versions and future versions of Forms once fixed, you can simply override the OnOptionsItemselected like this
public override bool OnOptionsItemSelected(IMenuItem item)
{
// check if the current item id
// is equals to the back button id
if (item.ItemId == 16908332)
{
// retrieve the current xamarin forms page instance
var currentpage = (MyAdvantagePage)
Xamarin.Forms.Application.
Current.MainPage.Navigation.
NavigationStack.LastOrDefault();
// check if the page has subscribed to
// the custom back button event
if (currentpage?.CustomBackButtonAction != null)
{
// invoke the Custom back button action
currentpage?.CustomBackButtonAction.Invoke();
// and disable the default back button action
return false;
}
// if its not subscribed then go ahead
// with the default back button action
return base.OnOptionsItemSelected(item);
}
else
{
// since its not the back button
//click, pass the event to the base
return base.OnOptionsItemSelected(item);
}
}
However, if you are using FormsAppCompatActivity, then you need to add onto your OnCreate in MainActivity this to set your toolbar:
Android.Support.V7.Widget.Toolbar toolbar = this.FindViewById<Android.Support.V7.Widget.Toolbar>(Resource.Id.toolbar);
SetSupportActionBar(toolbar);
But wait! If you have too old a version of .Forms or too new version, a bug will come up where toolbar is null. If this happens, the hacked together way I got it to work to make a deadline is like this. In OnCreate in MainActivity:
MobileApp.Pages.Articles.ArticleDetail.androdAction = () =>
{
Android.Support.V7.Widget.Toolbar toolbar = this.FindViewById<Android.Support.V7.Widget.Toolbar>(Resource.Id.toolbar);
SetSupportActionBar(toolbar);
};
ArticleDetail is a Page, and androidAction is an Action that I run on OnAppearing if the Platform is Android on my page. By this point in your app, toolbar will no longer be null.
Couple more steps, the iOS render we made above uses properties that you need to add to whatever page you are making the renderer for. I was making it for my MyAdvantagePage class that I made, which implements ContentPage . So in my MyAdvantagePage class I added
public Action CustomBackButtonAction { get; set; }
public static readonly BindableProperty EnableBackButtonOverrideProperty =
BindableProperty.Create(
nameof(EnableBackButtonOverride),
typeof(bool),
typeof(MyAdvantagePage),
false);
/// <summary>
/// Gets or Sets Custom Back button overriding state
/// </summary>
public bool EnableBackButtonOverride
{
get
{
return (bool)GetValue(EnableBackButtonOverrideProperty);
}
set
{
SetValue(EnableBackButtonOverrideProperty, value);
}
}
Now that that is all done, on any of my MyAdvantagePage I can add this
:
this.EnableBackButtonOverride = true;
this.CustomBackButtonAction = async () =>
{
if (_browser.CanGoBack)
{
_browser.GoBack();
}
else
{
await Navigation.PopAsync(true);
}
};
That should be everything to get it to work on Android hardware back, and navigation back for both android and iOS.
You are right, in your page class override OnBackButtonPressed and return true if you want to prevent navigation. It works fine for me and I have the same version.
protected override bool OnBackButtonPressed()
{
if (Condition)
return true;
return base.OnBackButtonPressed();
}
Depending on what exactly you are looking for (I would not recommend using this if you simply want to cancel back button navigation), OnDisappearing may be another option:
protected override void OnDisappearing()
{
//back button logic here
}
OnBackButtonPressed() this will be called when a hardware back button is pressed as in android. This will not work on the software back button press as in ios.
Additional to Kyle Answer
Set
Inside YOURPAGE
public static Action SetToolbar;
YOURPAGE OnAppearing
if (Device.RuntimePlatform == Device.Android)
{
SetToolbar.Invoke();
}
MainActivity
YOURPAGE.SetToolbar = () =>
{
Android.Support.V7.Widget.Toolbar toolbar =
this.FindViewById<Android.Support.V7.Widget.Toolbar>(Resource.Id.toolbar);
SetSupportActionBar(toolbar);
};
I use Prism libray and for handle the back button/action I extend INavigatedAware interface of Prism on my page and I implement this methods:
public void OnNavigatedFrom(INavigationParameters parameters)
{
if (parameters.GetNavigationMode() == NavigationMode.Back)
{
//Your code
}
}
public void OnNavigatedTo(INavigationParameters parameters)
{
}
Method OnNavigatedFrom is raised when user press back button from Navigation Bar (Android & iOS) and when user press Hardware back button (only for Android).
For anyone still fighting with this issue - basically you cannot intercept back navigation cross-platform. Having said that there are two approaches that effectively solve the problem:
Hide the NavigationPage back button with NavigationPage.ShowHasBackButton(this, false) and push a modal page that has a custom Back/Cancel/Close button
Intercept the back navigation natively for each platform. This is a good article that does it for iOS and Android: https://theconfuzedsourcecode.wordpress.com/2017/03/12/lets-override-navigation-bar-back-button-click-in-xamarin-forms/
For UWP you are on your own :)
Edit:
Well, not anymore since I did it :) It actually turned out to be pretty easy – there is just one back button and it’s supported by Forms so you just have to override ContentPage’s OnBackButtonPressed:
protected override bool OnBackButtonPressed()
{
if (Device.RuntimePlatform.Equals(Device.UWP))
{
OnClosePageRequested();
return true;
}
else
{
base.OnBackButtonPressed();
return false;
}
}
async void OnClosePageRequested()
{
var tdvm = (TaskDetailsViewModel)BindingContext;
if (tdvm.CanSaveTask())
{
var result = await DisplayAlert("Wait", "You have unsaved changes! Are you sure you want to go back?", "Discard changes", "Cancel");
if (result)
{
tdvm.DiscardChanges();
await Navigation.PopAsync(true);
}
}
else
{
await Navigation.PopAsync(true);
}
}
protected override bool OnBackButtonPressed()
{
base.OnBackButtonPressed();
return true;
}
base.OnBackButtonPressed() returns false on click of hardware back button.
In order to prevent operation of back button or prevent navigation to previous page. the overriding function should be returned as true. On return true, it stays on the current xamarin form page and state of page is also maintained.
The trick is to implement your own navigation page that inherits from NavigationPage. It has the appropriate events Pushed, Popped and PoppedToRoot.
A sample implementation could look like this:
public class PageLifetimeSupportingNavigationPage : NavigationPage
{
public PageLifetimeSupportingNavigationPage(Page content)
: base(content)
{
Init();
}
private void Init()
{
Pushed += (sender, e) => OpenPage(e.Page);
Popped += (sender, e) => ClosePage(e.Page);
PoppedToRoot += (sender, e) =>
{
var args = e as PoppedToRootEventArgs;
if (args == null)
return;
foreach (var page in args.PoppedPages.Reverse())
ClosePage(page);
};
}
private static void OpenPage(Page page)
{
if (page is IPageLifetime navpage)
navpage.OnOpening();
}
private static void ClosePage(Page page)
{
if (page is IPageLifetime navpage)
navpage.OnClosed();
page.BindingContext = null;
}
}
Pages would implement the following interface:
public interface IPageLifetime
{
void OnOpening();
void OnClosed();
}
This interface could be implemented in a base class for all pages and then delegate it's calls to it's view model.
The navigation page and could be created like this:
var navigationPage = new PageLifetimeSupportingNavigationPage(new MainPage());
MainPage would be the root page to show.
Of course you could also just use NavigationPage in the first place and subscribe to it's events without inheriting from it.
Maybe this can be usefull, You need to hide the back button, and then replace with your own button:
public static UIViewController AddBackButton(this UIViewController controller, EventHandler ev){
controller.NavigationItem.HidesBackButton = true;
var btn = new UIBarButtonItem(UIImage.FromFile("myIcon.png"), UIBarButtonItemStyle.Plain, ev);
UIBarButtonItem[] items = new[] { btn };
controller.NavigationItem.LeftBarButtonItems = items;
return controller;
}
public static UIViewController DeleteBack(this UIViewController controller)
{
controller.NavigationItem.LeftBarButtonItems = null;
return controller;
}
Then call them into these methods:
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
this.AddBackButton(DoSomething);
UpdateFrames();
}
public override void ViewWillDisappear(Boolean animated)
{
this.DeleteBackButton();
}
public void DoSomething(object sender, EventArgs e)
{
//Do a barrel roll
}
Another way around is to use Rg.Plugins.Popup Which allows you to implement nice popup. It uses another NavigationStack => Rg.Plugins.Popup.Services.PopupNavigation.Instance.PopupStack. So your page won't be wrap around the NavigationBar.
In your case I would simply
Create a full page popup with opaque background
Override ↩️ OnBackButtonPressed for Android on ⚠️ParentPage⚠️ with something like this:
protected override bool OnBackButtonPressed()
{
return Rg.Plugins.Popup.Services.PopupNavigation.Instance.PopupStack.Any();
}
Since the back-button affect the usual NavigationStack your parent would pop out whenever the user try to use it while your "popup is showing".
Now what? Xaml what ever you want to properly close your popup with all the check you want.
💥 Problem solved for these targets💥
[x] Android
[x] iOS
[-] Windows Phone (Obsolete. Use v1.1.0-pre5 if WP is needed)
[x] UWP (Min Target: 10.0.16299)

Caliburn Micro Communication between ViewModels

hopefully you can help me. First of all, let me explain what my problem is.
I have two ViewModels. The first one has e.g. stored information in several textboxes.
For example
private static string _tbxCfgLogfile;
public string TbxCfgLogfile
{
get { return _tbxCfgLogfile; }
set
{
_tbxCfgLogfile = value;
NotifyOfPropertyChange(() => TbxCfgLogfile);
}
}
The other ViewModel has a Button where i want to save this data from the textboxes.
It does look like this
public bool CanBtnCfgSave
{
get
{
return (new PageConfigGeneralViewModel().TbxCfgLogfile.Length > 0 [...]);
}
}
public void BtnCfgSave()
{
new Functions.Config().SaveConfig();
}
How can i let "CanBtnCfgSave" know that the condition is met or not?
My first try was
private static string _tbxCfgLogfile;
public string TbxCfgLogfile
{
get { return _tbxCfgLogfile; }
set
{
_tbxCfgLogfile = value;
NotifyOfPropertyChange(() => TbxCfgLogfile);
NotifyOfPropertyChange(() => new ViewModels.OtherViewModel.CanBtnCfgSave);
}
}
It does not work. When i do remember right, i can get the data from each ViewModel, but i cannot set nor Notify them without any effort. Is that right? Do i have to use an "Event Aggregator" to accomplish my goal or is there an alternative easier way?
Not sure what you are doing in your viewmodels - why are you instantiating viewmodels in property accessors?
What is this line doing?
return (new PageConfigGeneralViewModel().TbxCfgLogfile.Length > 0 [...]);
I can't be sure from your setup as you haven't mentioned much about the architecture, but sincce you should have an instance of each viewmodel, there must be something conducting/managing the two (or one managing the other)
If you have one managing the other and you are implementing this via concrete references, you can just pick up the fields from the other viewmodel by accessing the properties directly, and hooking the PropertyChanged event of the child to notify the parent
class ParentViewModel : PropertyChangedBase
{
ChildViewModel childVM;
public ParentViewModel()
{
// Create child VM and hook up event...
childVM = new ChildViewModel();
childVM.PropertyChanged = ChildViewModel_PropertyChanged;
}
void ChildViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
// When any properties on the child VM change, update CanSave
NotifyOfPropertyChange(() => CanSave);
}
// Look at properties on the child VM
public bool CanSave { get { return childVM.SomeProperty != string.Empty; } }
public void Save() { // do stuff }
}
class ChildViewModel : PropertyChangedBase
{
private static string _someProperty;
public string SomeProperty
{
get { return _someProperty; }
set
{
_someProperty = value;
NotifyOfPropertyChange(() => SomeProperty);
}
}
}
Of course this is a very direct way to do it - you could just create a binding to CanSave on the child VM if that works, saving the need to create the CanSave property on the parent

MVVM - View loading and eventhandling

In my windows phone app, I need to track some events to get a good flow. But I'm not sure how to handle them in good sequence.
What needs to be done at startup of the app:
Main view is loaded and corresponding view model instantiated
In the constructor of the view model I initiate a login sequence that signals when completed with an eventhandler
Now when the login sequence has finished AND the view is completely loaded I need to startup another sequence.
But here is the problem, the order of these 2 events 'completing' is not always the same...
I've use the EventToCommand from MVVMLight to signal the view model that the view has 'loaded'.
Any thoughts on how to synchronize this.
As you should not use wait handles or something similar on the UI thread. You will have to sync the two method using flags in your view model and check them before progressing.
So, implement two boolean properties in your view model. Now when the login dialog is finished set one of the properties (lets call it IsLoggedIn) to true, and when the initialization sequence is finished you set the other property (how about IsInitialized) to true. The trick now lies in the implementation of the setter of these two properties:
#region [IsInitialized]
public const string IsInitializedPropertyName = "IsInitialized";
private bool _isInitialized = false;
public bool IsInitialized {
get {
return _isInitialized;
}
set {
if (_isInitialized == value)
return;
var oldValue = _isInitialized;
_isInitialized = value;
RaisePropertyChanged(IsInitializedPropertyName);
InitializationComplete();
}
}
#endregion
#region [IsLoggedIn]
public const string IsLoggedInPropertyName = "IsLoggedIn";
private bool _isLoggedIn = false;
public bool IsLoggedIn {
get {
return _isLoggedIn;
}
set {
if (_isLoggedIn == value)
return;
var oldValue = _isLoggedIn;
_isLoggedIn = value;
RaisePropertyChanged(IsLoggedInPropertyName);
InitializationComplete();
}
}
#endregion
public void InitializationComplete() {
if (!(this.IsInitialized && this.IsLoggedIn))
return;
// put your code here
}
Alternatively you can remove the InitializationComplete from the setters and change InitializationComplete to:
public void InitializationComplete() {
// put your code here
}
Then subscribe to the 'PropertyChanged' event use the following implementation:
private void Class1_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) {
if (e.PropertyName == IsInitializedPropertyName || e.PropertyName == IsLoggedInPropertyName) {
if (this.IsInitialized && this.IsLoggedIn)
InitializationComplete();
}
}

Resources