NavigateCommand only called once - c#

In order to familiarize myself with MVVM for WinRT I have been looking at the example MvvmLight WinRT project. Currently I'm running into an issue where my RelayCommand is only called once (on construction of viewmodel). What I want to do is go to the MainViewModel if the user is authorized. If I remove the conditional check of the login in the LoginCommand method, the RelayCommand works as expected. Any thoughts as to what I'm doing wrong? Should I not being doing my validation within the LoginCommand?
LoginViewModel (some code has been removed):
public class LoginViewModel : ViewModelBase {
private readonly IDataService _dataService;
private readonly INavigationService _navigationService;
private RelayCommand _navigateCommand;
private Login login; //contains username and password
/// <summary>
/// Gets the NavigateCommand.
/// THIS DOES NOT GET FIRED UPON BUTTON CLICK
/// </summary>
public RelayCommand LoginCommand{
get {
if (login != null && login.UserName.Equals("Test"))
return _navigateCommand ?? (_navigateCommand = new RelayCommand(() => _navigationService.Navigate(typeof(MainPage))));
return _navigateCommand;
}
}
LoginPage.xaml.cs (some code has been removed):
public sealed partial class LoginPage {
public LoginViewModel Vm {
get { return (LoginViewModel)DataContext; }
}
public LoginPage() {
InitializeComponent();
}
protected override void LoadState(object state) {
var casted = state as LoginPageState;
if (casted != null) {
Vm.Load(casted);
}
}
protected override object SaveState() {
return new LoginPageState {
Credentials = new Login {
UserName = txtUserName.Text,
Password = txtPassword.Text
}
};
}
public class LoginPageState {
public Login Credentials { get; set; }
}
}
}
LoginPage.xaml (some code has been removed)
<Button Content="Login"
x:Name="NextPageButton"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Margin="10"
Command="{Binding LoginCommand}" />

The problem is the condition. When your LoginPage loads, it tries to bind your Button to the LoginCommand. In order to achieve that, it gets the LoginCommand from your ViewModel. At that point in time Login is null and therefore the property will return _navigateCommand which is null. After that the page will not try to use the LoginCommand because it already knows its value.
To solve this you could move the condition inside the lambda expression:
public RelayCommand LoginCommand
{
get
{
return _navigateCommand ?? (_navigateCommand = new RelayCommand(
() =>
{
if (login != null && login.UserName.Equals("Test"))
{
_navigationService.Navigate(typeof(MainPage));
}
}));
}
}
An even better solution would be to move the authorization to another class. Something like:
public RelayCommand LoginCommand
{
get
{
return _navigateCommand ?? (_navigateCommand = new RelayCommand(
() =>
{
if (_authorizationService.UserAuthorized(login))
{
_navigationService.Navigate(typeof(MainPage));
}
}));
}
}
Hope this helps.

Related

How to disable a button if textbox and passwordbox is blank in wpf?

I basically used a Model's (UserAccount) Property from my ViewModel(CreateAccountViewModel) to bind to my View, and call to my Command (CreateAccountCommand).
My Model(UserAccount):
public class UserAccount : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private int _id;
private string _username;
private string _password;
private DateTime _dateTime;
public int Id
{
get { return _id; }
set { _id = value; OnPropertyChanged(nameof(Id)); }
}
public string Username
{
get { return _username; }
set { _username = value; OnPropertyChanged(nameof(Username)); }
}
public string Password
{
get { return _password; }
set { _password = value; OnPropertyChanged(nameof(Password)); }
}
public DateTime DateCreated
{
get { return _dateTime; }
set { _dateTime = value; OnPropertyChanged(nameof(DateCreated)); }
}
public virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
My ViewModel(CreateAccountViewModel):
public class CreateAccountViewModel: ViewModelBase
{
private UserAccount _userAccount;
public UserAccount CurrentUserAccount
{
get { return _userAccount; }
set { _userAccount = value; OnPropertyChanged(nameof(CurrentUserAccount)); }
}
public ICommand CreateAccountCommand{ get; }
public CreateAccountViewModel()
{
CreateAccountCommand= new CreateAccountCommand(this, Test);
CurrentUserAccount = new UserAccount();
}
public void Test()
{
MessageBox.Show("Random Message");
//I'm going to put my Create functionality here
}
}
My View (CreateAccountView):
<!--The TextBox for username-->
<TextBox Grid.Column="1"
Margin="10,0,0,0"
Text="{Binding Path=CurrentUserAccount.Username, UpdateSourceTrigger=PropertyChanged}" />
<!--The PasswordBox for password-->
<components:BindablePasswordBox Grid.Column="1"
Margin="10,0,0,0"
Password="{Binding Path=CurrentUserAccount.Password, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
Grid.ColumnSpan="2" />
<!--The Create user button-->
<Button Grid.Row="2"
Margin="0,20,0,0"
HorizontalAlignment="Center"
Command="{Binding CreateAccountCommand}"
Content="Create Account" />
My Command(CreateAccountCommand):
public class CreateAccountCommand: ICommand
{
private readonly CreateAccountViewModel _viewModel;
private readonly Action RunCommand;
public CreateAccountCommand(CreateAccountViewModel viewModel , Action runCommand)
{
_viewModel = viewModel;
_viewModel.PropertyChanged += ViewModel_PropertyChanged;
RunCommand = runCommand;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
//This is supposed to check whether the Username textbox and Password passwordbox is blank (if both of them are blank, the button should be disabled, else disabled
return !string.IsNullOrEmpty(_viewModel.CurrentUserAccount.Username) && !string.IsNullOrEmpty(_viewModel.CurrentUserAccount.Password);
}
public void Execute(object parameter)
{
RunCommand();
}
private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
CanExecuteChanged?.Invoke(this, new EventArgs());
}
}
My PasswordBox is bindable because I created a custom PasswordBox with DependencyProperty:
public partial class BindablePasswordBox : UserControl
{
public static readonly DependencyProperty PasswordProperty =
DependencyProperty.Register("Password", typeof(string), typeof(BindablePasswordBox),
new PropertyMetadata(string.Empty));
public string Password
{
get { return (string)GetValue(PasswordProperty); }
set { SetValue(PasswordProperty, value); }
}
public BindablePasswordBox()
{
InitializeComponent();
}
//This method will notify us, whenever a password in our passwordBox changes
private void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
{
Password = passwordBox.Password; //sets the value of the DependencyProperty (PasswordProperty)
}
}
My problem here, is that, the button in my View does not change enable/disable even if I set my command's CanExecute to do so. Am I missing something obvious here? I really have to ask because I've been stuck here since yesterday. (My Main goal here is to disable the Create Account button if the Textbox and PasswordBox have no input. Any solutions are okay)
Lets do a small refactoring.
use CallerMemberNameAttribute (see here how) to have shorter property setters in vm;
write once reusable ICommand implementation and use it for all commands, see DelegateCommand;
rise command CanExecuteChanged in vm when you change one of command canExecuted condition;
UserAccount needs notifications (you have done it in the edit), if it's a model, then you need an extra vm to act as a wrapper, otherwise you wouldn't be able to catch changes done by the bound controls;
Since the properties of UserAccount are part of command canExecuted, you need to monitor for them.
With all changes your button using the command should be property enabled/disabled.
Below is pseudo-code (can contain mistakes):
public class CreateAccountViewModel: ViewModelBase
{
UserAccount _userAccount;
public UserAccount CurrentUserAccount
{
get => _userAccount;
set
{
// unsubscribe
if(_userAccount != null)
_userAccount -= UserAccount_PropertyChanged;
_userAccount = value;
// subscribe
if(_userAccount != null)
_userAccount += UserAccount_PropertyChanged;
// notifications
OnPropertyChanged(); // shorter syntax with CallerMemberNameAttribute
CreateAccountCommand.RaiseCanExecuteChanged();
}
}
public ICommand CreateAccountCommand { get; }
public CreateAccountViewModel()
{
CurrentUserAccount = new UserAccount();
CreateAccountCommand = new DelegateCommand(Test,
o => !string.IsNullOrEmpty(CurrentUserAccount.Username) && !string.IsNullOrEmpty(CurrentUserAccount.Password));
}
void Test(object parameter)
{
MessageBox.Show("Random Message");
//I'm going to put my Create functionality here
}
void UserAccount_PropertyChanged(object sender, NotifyPropertyChangedEventArgs e) =>
CreateAccountCommand.RaiseCanExecuteChanged(); // rise always of check for specific properties changes
}
The CreateAccountCommand hooks up en event handler to the view model's PropertyChanged but there is no such event raised when you set the Username and Password properties of the UserAccount object.
Either implement INotifyPropertyChanged in UserAccount or bind to wrapper properties of the CreateAccountViewModel:
public string Username
{
get { return _userAccount?.Username; }
set
{
if (_userAccount != null)
_userAccount.Username = value;
OnPropertyChanged();
}
}
If you decide to implement INotifyPropertyChanged in UserAccount, you still need to notify the command when the properties have been updated.
Since your CurrentUserAccount property may be set to a new value dynamically, you should remove and add the event handler dynamically:
private UserAccount _userAccount;
public UserAccount CurrentUserAccount
{
get { return _userAccount; }
set
{
if (_userAccount != null)
_userAccount.PropertyChanged -= OnUserAccountPropertyChanged;
_userAccount = value;
if (_userAccount != null)
_userAccount.PropertyChanged += OnUserAccountPropertyChanged;
OnPropertyChanged(nameof(CurrentUserAccount));
}
}
private void OnUserAccountPropertyChanged(object sender, PropertyChangedEventArgs e) =>
OnPropertyChanged(null);

Navigate between Login and Dashboard page

I'm new to WPF. I'm creating a POS desktop application by using WPF MVVM pattern as front-end development. (I have try my best to make this question as short as possible.)
Scenario: I have a MainViewModel which will show AuthView (and AuthViewModel) by default whenever user open the application. After user fill in the form and click the Login button in AuthView, LoginCommand will be called on the view, if login successful, they will be redirect to DashboardView.
MainWindow.xaml
<Grid>
<ContentControl Content="{Binding SelectedViewModel}"/>
</Grid>
MainViewModel.cs
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
if (SelectedViewModel == null)
{
SelectedViewModel = new AuthViewModel();
}
else
{
SelectedViewModel = new DashboardViewModel();
}
}
private ViewModelBase _selectedViewModel;
public ViewModelBase SelectedViewModel
{
get { return _selectedViewModel; }
set { _selectedViewModel = value; OnPropertyChanged(nameof(SelectedViewModel)); }
}
public void ChangeToDashboard()
{
SelectedViewModel = new DashboardViewModel();
}
}
AuthViewModel.cs
public class AuthViewModel : ViewModelBase
{
public AuthViewModel()
{
loginCommand = new RelayCommand(Login);
}
#region Login
private RelayCommand loginCommand;
public RelayCommand LoginCommand
{
get { return loginCommand; }
}
private async void Login()
{
try
{
Response = await callLoginAPI; //some custom login occurs here
if (Response.Status == "ok")
{
//change viewModel to DashboardViewModel screen
MainViewModel MainViewModel = new MainViewModel();
MainViewModel.ChangeToDashboard();
}
}
catch (Exception e)
{
//
}
}
#endregion
}
Problem: I have go through a lot of SA solution but still unable to switch the view after user login successfully.
Question: How can I trigger the MainViewModel to change UI after I have change the SelectedViewModel property (after user login successfully, response.status == ok)? or is there any other better (as simple as possible) way to achieve what I am trying to do?
AuthViewModel can generate event about login
public class AuthViewModel : ViewModelBase
{
public AuthViewModel()
{
loginCommand = new RelayCommand(Login);
}
public event EventHandler LoginCompleted;
protected virtual void OnLoginCompleted(EventArgs e)
{
EventHandler handler = LoginCompleted;
handler?.Invoke(this, e);
}
private RelayCommand loginCommand;
public RelayCommand LoginCommand
{
get { return loginCommand; }
}
private async void Login()
{
try
{
Response = await callLoginAPI(); //some custom login occurs here
if (Response.Status == "ok")
{
OnLoginCompleted(EventArgs.Empty);
}
}
catch (Exception e)
{
//
}
}
}
and MainViewModel can handle that event:
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
if (SelectedViewModel == null)
{
var vm = new AuthViewModel();
vm.LoginCompleted += (sender, e) => ChangeToDashboard();
SelectedViewModel = vm;
}
else
{
SelectedViewModel = new DashboardViewModel();
}
}
private ViewModelBase _selectedViewModel;
public ViewModelBase SelectedViewModel
{
get { return _selectedViewModel; }
set { _selectedViewModel = value; OnPropertyChanged(nameof(SelectedViewModel)); }
}
private void ChangeToDashboard()
{
SelectedViewModel = new DashboardViewModel();
}
}

Why is this delegate variable null?

I want to pass a method in the MainViewModel to a delegate variable in the LoginViewModel object like this:
public class ApplicationViewModel : INotifyPropertyChanged
{
private LoginViewModel loginViewModel;
public ApplicationViewModel()
{
loginViewModel = new LoginViewModel();
this.loginViewModel.Login += this.checkLoginData; //stays null..
CurrentPageViewModel = this.loginViewModel; //works fine
}
private void checkLoginData(string username, string password)
{
//validating data
}
}
But for some reason, the loginViewModel.Login is null...
And this Command in the LoginViewModel keeps firing this at start, and telling me that the Login == null, which is not what I expect because I initialize the delegate at the MainViewModel constructor.
I'm not a expert at MVVM/WPF, but I trying to work for it.
EDIT: extra information.
And loginViewModel.Login is a delegate variable like this:
class LoginViewModel : ObservableObject, IPageViewModel
{
public delegate void DelegateLogin(string username, string password);
private DelegateLogin _login;
public DelegateLogin Login
{
get { return this._login; }
set
{
/*
if(this._login != value)
{
this._login = value;
OnPropertyChanged("Login");
}*/
this._login = value;
OnPropertyChanged("Login");
}
}
public ICommand CheckLoginCommand
{
get
{
if (Login != null)
{
this.checkLoginCommand = new Command(p => { Login(this._username, this._password); });
}
else
{
System.Windows.MessageBox.Show("Login DELEGATE IS EMPTY!?!?!"); //keeps firing...
}
return this.checkLoginCommand;
}
}
}
Try this:
public ICommand CheckLoginCommand
{
get
{
if (this.checkLoginCommand == null)
this.checkLoginCommand = new Command(p => {
if (Login != null)
Login(this._username, this._password);
else
System.Windows.MessageBox.Show("Login DELEGATE IS EMPTY!?!?!"); //keeps firing...
});
return this.checkLoginCommand;
}
}
This has the advantage of creating the command regardless of whether the Login delegate set. Other things aside, hopefully Login will be ready by the time the command gets invoked.

reuse code for RelayCommand

I'm using MVVM light for a WPF application. I have a view model with several commands that use the RelayCommand. Since the code is very similar for each command, I created a GetCommand Method. But the resulting RelayCommand does not work if I use the param inside the RelayCommand. If I don't use the param everything works fine (except that I can't pass a value).
Can someone explain why this happens and what other solution there is to reuse the code without copy & paste?
Below is a very reduced version of my code that shows only the important parts:
public class MainViewModel {
public RelayCommand commandOne = GetCommand("one");
public RelayCommand commandTwo = GetCommand("two");
public RelayCommand GetCommand(string param) {
return new RelayCommand(() => {
// Do something accessing other properties of MainViewModel
// to detect if another action is alreay running
// this code would need to be copy & pasted everywhere
if(param == "one")
_dataService.OneMethod();
else if(param == "two")
_dataService.TwoMethod();
else
_dataService.OtherMethod();
var name = param;
});
}
}
This is how I usually use RelayCommands where I just bind the commands to methods.
public class MainViewModel {
public MainViewModel()
{
CommandOne = new RelayCommand<string>(executeCommandOne);
CommandTwo = new RelayCommand(executeCommandTwo);
}
public RelayCommand<string> CommandOne { get; set; }
public RelayCommand CommandTwo { get; set; }
private void executeCommandOne(string param)
{
//Reusable code with param
}
private void executeCommandTwo()
{
//Reusable code without param
}
}
You may be looking for something like the following
public partial class MainWindow : Window
{
private RelayCommand myRelayCommand ;
private string param = "one";
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
}
public RelayCommand MyRelayCommand
{
get
{
if (myRelayCommand == null)
{
myRelayCommand = new RelayCommand((p) => { ServiceSelector(p); });
}
return myRelayCommand;
}
}
private void DoSomething()
{
MessageBox.Show("Did Something");
}
private void ServiceSelector(object p)
{
DoSomething();
if (param == "one")
MessageBox.Show("one");
else if (param == "two")
MessageBox.Show("two");
else
MessageBox.Show("else");
var name = param;
}
}

Very very simple MVVM problem

I am trying to make my very first Silverlight App ever, but I can't get the LogOn function to work, can you help me? This should properly be super simple for all of you, I will show you my two files: LogOn.xaml.cs and LogOnViewModel.cs
Apparently the problem is that UserId gets not set early enough to be availble in LogOn.xaml.cx when I need it, can you help me make it work, that would lift my moment quite a bit :-)
public partial class LogOn : PhoneApplicationPage
{
public LogOn()
{
InitializeComponent();
this.DataContext = LogOnViewModel.Instance;
}
private void btnLogOn_Click(object sender, RoutedEventArgs e)
{
if ((!string.IsNullOrEmpty(txtEmailAddress.Text)) && (!string.IsNullOrEmpty(txtPassword.Password)))
{
txbLogonMessage.Text = "";
LogOnViewModel.Instance.UserLogin(txtEmailAddress.Text, txtPassword.Password);
if (LogOnViewModel.Instance.UserId > 0)
NavigationService.Navigate(new Uri("/_2HandApp;component/Views/Main.xaml", UriKind.Relative));
else
txbLogonMessage.Text = "Login was unsuccessful. The user name or password provided is incorrect. Please correct the errors and try again. ";
}
}
}
public sealed class LogOnViewModel : INotifyPropertyChanged
{
public static LogOnViewModel Instance = new LogOnViewModel();
//public static int userId;
private SHAServiceClient WS;
private int userId;
public int UserId
{
get
{
return userId;
}
set
{
userId = value;
this.RaisePropertyChanged("UserId");
}
}
private LogOnViewModel()
{
WS = new SHAServiceClient();
WS.UserLoginCompleted += new EventHandler<UserLoginCompletedEventArgs>(WS_UserLoginCompleted);
}
void WS_UserLoginCompleted(object sender, UserLoginCompletedEventArgs e)
{
if (e.Error == null)
{
this.UserId = e.Result;
}
}
public void UserLogin(string email, string password)
{
WS.UserLoginAsync(email, password);
}
/* Implementing the INotifyPropertyChanged interface. */
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler propertyChanged = this.PropertyChanged;
if ((propertyChanged != null))
{
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
The cause of the problem is what has been highlighted by #flq. You're making an asynchronous call, meaning that you won't get the expected result right away (in your case, the UserId being assigned), but instead, you can subscibe to the Completed event (or provide a callback) to handle things when the asynchronous task finishes.
Now, the "MVVM way" to do this (or at least what I would do) is as follows: first of all, go get MVVM Light! it's a lightweight MVVM framework which would be very helpful. You should have your ViewModel class implement the ViewModelBase base class from MVVMLight, this would provide the change notification and messaging as well as other useful stuff. Then, you should encapsulate the login functionality in a command to be able to wire up it up from xaml, for that you can use MVVMLight's RelayCommand. Once the login is complete, you can just send a message to your view letting it know that (in a pretty decoupled way), and the view can simply initiate the navigation.
Here's the bits of code for that:
public class LogOnViewModel : ViewModelBase
{
private SHAServiceClient WS;
public LogOnViewModel()
{
WS = new SHAServiceClient();
WS.UserLoginCompleted += new EventHandler<UserLoginCompletedEventArgs>(WS_UserLoginCompleted);
LoginCommand = new RelayCommand(UserLogin);
}
private int userId;
public int UserId
{
get { return userId; }
set
{
userId = value;
RaisePropertyChanged(()=>UserId);
}
}
private int password;
public int Password
{
get { return password; }
set
{
password = value;
RaisePropertyChanged(()=>Password);
}
}
private int username;
public int Username
{
get { return username; }
set
{
username = value;
RaisePropertyChanged(()=>Username);
}
}
private int loginErrorMessage;
public int LoginErrorMessage
{
get { return loginErrorMessage; }
set
{
loginErrorMessage = value;
RaisePropertyChanged(()=>LoginErrorMessage);
}
}
void WS_UserLoginCompleted(object sender, UserLoginCompletedEventArgs e)
{
if (e.Error == null)
{
this.UserId = e.Result;
// send a message to indicate that the login operation has completed
Messenger.Default.Send(new LoginCompleteMessage());
}
}
public RelayCommand LoginCommand {get; private set;}
void UserLogin()
{
WS.UserLoginAsync(email, password);
}
}
for the xaml:
<TextBox Text="{Binding Username, Mode=TwoWay}"/>
<TextBox Text="{Binding Password, Mode=TwoWay}"/>
<Button Command="{Binding LoginCommand}"/>
<TextBlock Text="{Binding LoginErrorMessage}"/>
in the code behind:
public partial class LogOn : PhoneApplicationPage
{
public LogOn()
{
InitializeComponent();
this.DataContext = new LogOnViewModel();
Messenger.Default.Register<LoginCompletedMessage>(
this,
msg=> NavigationService.Navigate(
new Uri("/_2HandApp;component/Views/Main.xaml",
UriKind.Relative) );
}
....
}
You can see that there is a little bit more (but straightforward) code in the ViewModel and less in the code behind. This also took advantage of DataBinding which is in the heart of MVVM.
Hope this helps :)
P.S: the LoginCompletedMessage class is just an empty class in this case (used just to define the type message), but you can use it to send more info (maybe you still want to have the UserId sent)
Well, you're calling an async version of a login WS.UserLoginAsync, which means the execution moves on and indeed there is no user id when you check for it.
You aren't really doing MVVVM here, but anyway, let's go with the flow. Have an event on your "Viewmodel" that is raised when the login process is finished (WS_UserLoginCompleted). You can handle it and trigger Navigation in an event-handler of that event.

Categories

Resources