Switching through different Pages in MVVM - c#

I am trying to implement the ability to switch through different Pages in my Xamarin MVVM project. I have three folders - "Models", "Views" and "ViewModels". "Views" contains "MainView" which's role is to display other Views.
MainView.xaml:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="KeepFit.Views.MainView">
<ContentPresenter x:Name="ContentArea" Content="{Binding CurrentView}"/>
</ContentPage>
MainView.xaml.cs:
public partial class MainView : ContentPage
{
public MainView()
{
InitializeComponent();
BindingContext = new MainViewModel();
}
}
As you can see, "MainView" is binded to "MainViewModel" class.
MainViewModel.cs:
public class MainViewModel : INotifyPropertyChanged
{
public Command SwitchViewsCommand { get; private set; }
public MainViewModel()
{
SwitchViewsCommand = new Command((parameter) =>
CurrentView = (ContentPage)Activator.CreateInstance(parameter as Type));
CurrentView = new HomeView();
}
private ContentPage _currentView;
public ContentPage CurrentView
{
get
{
return _currentView;
}
set
{
if (value != _currentView)
{
_currentView = value;
OnPropertyChanged("CurrentView");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName]
string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
The CurrentView is set to my HomeView (let us say it is default ContentPage) in the constructor. It is binded to the ContentPresenter, so it should be visible at the beginning of my application's runtime. But it is not.
I have noticed that the ContentPresenter is expecting the Xamarin.Forms.View object as the "Content" - and my Views are inheriting from Xamarin.Forms.ContentPage. But if I change them to ContentViews for example - I will not be able to set the BindingContext for them. Could anybody explain what am I doing wrong?

You are try this?
async void butonClick(object sender, EventArgs e)
{
await Navigation.PushAsync(new EntryPageCode());
}
Your view instance in location of "EntryPageCode", ok.
Attempt this and tell me about, if work's be fine to you.

Related

How to make changes to objects in the page above

I have a NavigationView that loads a page into its associated frame.
On the loaded page there is a button, how can I use this button to change the properties of the NavigationView.
I know that to update the page in the frame is:
Frame.Navigate(typeof(Window2));
So I thought it might be:
Frame.NavigationView.IsEnabled = False
But this isn't valid.
Is there a way to do this?
You can not set NavigationView.IsEnabled directly from loaded page.You can set a property in a viewmodel to bind with the IsEnabled of navigationView.When you navigate a new page by Frame.Navigate(),pass the viewmodel to the new page.You can set the property to false when you click the button in the page.In this case,the navigationView will be disabled.
.xaml:
<NavigationView IsEnabled="{x:Bind MyViewModel.IsEnabled,Mode=OneWay}">
<NavigationView.MenuItems>
<NavigationViewItem Content="Item 1"></NavigationViewItem>
</NavigationView.MenuItems>
<Frame x:Name="ContentFrame"/>
</NavigationView>
.cs:
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private bool isEnabled = false;
public bool IsEnabled {
get { return isEnabled; }
set {
isEnabled = value;
OnPropertyChanged();
}
}
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public sealed partial class MainPage: Page
{​
public MainPage()​
{​
this.InitializeComponent();​
MyViewModel = new ViewModel();
}
private ViewModel MyViewModel { get; set; }
}
When you want to navigate a new page,pass the viewmodel:
ContentFrame.Navigate(typeof(Page1),MyViewModel);
Page1.cs:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
isEnabledVM = (e.Parameter as ViewModel);
}
private ViewModel isEnabledVM { get; set; }
private async void Button_Click(object sender, RoutedEventArgs e)
{
isEnabledVM.IsEnabled = false;
}

NotifyPropertyChanged and ContentControl

I am struggling for one week now with my problem and i cant solve it. I am programming a MVVM WPF application which is having one window (MainView). In This Mainview i want to load different UserControl when i need them. In the Application-Startup I am loading the MainViewModel. In the Constructor of the MainViewModel I am loading the First ViewModel (LoginViewModel). Cause of my MainView.xaml it is showing me my Login-User-Control like i want to. So till this point everything is fine. In the ActivePanel-class i am saving the CurrentView, because in my MainView.xaml i am making a binding to my CurrentView for the ContentControl. So everything is working except the changing of the views although my NotifyPropertyChanged method of the CurrentView is working. I am thinking, that my mistake is in the xaml (DataBinding). Hope you guys can help me.
This is my MainView.xaml in which i want to load the different DataTemplates. Like I said before: The loading of the LoginViewModel via the Constructor of MainViewModel is working. The changing to other VieModels is working as well, but the DataBinding to the ContentControl is the big problem here.
<Window x:Class="App.View.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:App.View"
mc:Ignorable="d"
xmlns:viewmodels="clr-namespace:App.ViewModels"
xmlns:views="clr-namespace:App.View"
xmlns:helper="clr-namespace:App.Helper"
Title="Betrooms" Height="500" Width="350">
<Window.Resources>
<DataTemplate x:Name="LoginUCTemplate" DataType="{x:Type viewmodels:LoginViewModel}">
<views:LoginUC DataContext="{Binding}"/>
</DataTemplate>
<DataTemplate x:Name="RegUCTemplate" DataType="{x:Type viewmodels:RegViewModel}">
<views:RegUC DataContext="{Binding}"/>
</DataTemplate>
<DataTemplate x:Name="HomeUCTemplate" DataType="{x:Type viewmodels:HomeViewModel}">
<views:HomeUC DataContext="{Binding}"/>
</DataTemplate>
</Window.Resources>
<Window.DataContext>
<viewmodels:ActivePanel/>
</Window.DataContext>
<Grid>
<ContentControl Content="{Binding CurrentView, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}"/>
</Grid>
</Window>
This is the class of my ActivePanel where i am saving the information about which ViewModel is the active one. The CurrentView is the property I am binding the Content Control to.
namespace APP.ViewModels
{
public class ActivePanel : NotifyPropertyChanged
{
private object _currentView;
public object CurrentView
{
get { return _currentView; }
set
{
if (value != _currentView)
{
_currentView = value;
OnPropertyChanged("CurrentView");
}
}
}
}
}
This is my MainViewModel:
namespace App.ViewModels
{
public class MainViewModel : ActivePanel
{
private LoginViewModel _loginViewModel;
public MainViewModel()
{
_loginViewModel = new LoginViewModel();
CurrentView = _loginViewModel;
}
}
}
And this is my LoginViewModel where I am changing the value of CurrentView via an action:
namespace App.ViewModels
{
public class LoginViewModel : ActivePanel
{
#region Member
private string _username;
private string _password;
bool login = false;
private HomeViewModel _homeViewModel;
private RegViewModel _regViewModel;
UserModel User = new UserModel();
#endregion
#region Properties
public ICommand RegActionCommand { get; set; }
public ICommand LogActionCommand { get; set; }
public string GetUsername
{
get { return _username; }
set
{
if (value != _username)
{
_username = value;
OnPropertyChanged("GetUsername");
}
}
}
public string GetPassword
{
get { return _password; }
set
{
if (value != _password)
{
_password = value;
OnPropertyChanged("GetPassword");
}
}
}
#endregion
#region Constructor
public LoginViewModel()
{
this.RegActionCommand = new RelayCommand(RegAction);
this.LogActionCommand = new RelayCommand(LogAction);
}
#endregion
#region Button-Action
private void LogAction(object obj)
{
_homeViewModel = new HomeViewModel();
CurrentView = _homeViewModel;
}
private void RegAction(object obj)
{
_regViewModel = new RegViewModel();
CurrentView = _regViewModel;
}
#endregion
}
}
I hope my question is understandable: The ContenControl binding is set to CurrentView but the ContentControl is never changing although the property of CurrentView is changing.
Thanks to you all. Cheers, Paul.
In your command handler, you are changing the CurrentView property of your LoginViewModel. The ContentControl's DataContext is the MainViewModel though, so it's content is bound to the CurrentView property of the MainViewModel.
You need to set the MainViewModel's property in your command handler. There are different ways of achieving this, for example you could add a constructor parameter to the LoginViewModel to pass a reference to the MainViewModel. You can save this reference and then access it in your command handler.
Another possibilty would be to raise an event or send a message from the command in your LoginViewModel and handle it in the MainViewModel. This would reduce the coupling between your ViewModels, but depending on which mechanism and library you use it might be a little bit more complicated.

ListView ItemsSource binding not displaying items

I am trying to create a bound ListView in Xamarin. Here's the C# code:
public partial class LearnPage : ContentPage
{
public class TodoItem
{
public string DisplayName { get; set; }
}
//ObservableCollection<TodoItem> Items = new ObservableCollection<TodoItem>();
private IEnumerable<TodoItem> _items;
public IEnumerable<TodoItem> Items
{
get { return _items; }
set
{
if (Equals(_items, value))
return;
_items = value;
OnPropertyChanged();
}
}
public LearnPage ()
{
InitializeComponent();
BindingContext = this;
Items = new TodoItem[]{
new TodoItem{ DisplayName = "Milk cartons are recyclable" }
};
//Items.Add(new TodoItem { DisplayName = "Milk cartons are recyclable" });
}
}
You can also see some commented out code with an ObervableCollection, which I have also tried with.
And here's the XAML:
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="learn.LearnPage">
<ContentPage.Content>
<StackLayout VerticalOptions="FillAndExpand" Padding="0,10,0,10">
<ListView ItemsSource="{Binding Items}" RowHeight="40" x:Name="sarasas">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding DisplayName}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage.Content>
When I build the app, an empty list is displayed. I'm really new to Xamarin and I think I'm just missing something obvious. Any help appreciated!
I am not sure if ContentPage uses [CallerMemberName] for the OnPropertyChanged() method. So first thing I would try is to write OnPropertyChanged("Items") instead.
Either way, if I were you I would separate concerns and move the Items into a ViewModel class, which implements INotifyPropertyChanged itself. This will help later on if you want to test, add more code such as commands, inject dependencies etc., where you will keep ViewModel code separate from View code.
So you could start with:
public abstract class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged ([CallerMemberName]string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs (propertyName));
}
}
Then create your ViewModel:
public class LearnViewModel : BaseViewModel
{
public ObservableCollection<TodoItem> Items { get; } = new ObservableCollection<TodoItem>();
private ICommand _addTodoItem;
public ICommand AddTodoItem =>
_addTodoItem = _addTodoItem ?? new Command(DoAddTodoItem);
private int _count;
private void DoAddTodoItem()
{
var item = new TodoItem { DisplayName = $"Item {++_count}" };
// NotifyCollectionChanged must happen on UI thread.
Device.BeginInvokeOnMainThread (() => {
Items.Add(item);
});
}
}
Then you can keep your View code thin like:
public partial class LearnPage : ContentPage
{
public LearnPage ()
{
InitializeComponent();
BindingContext = new LearnViewModel();
}
}
Normally you would invoke a command to populate the Items in your ViewModel, this could be by fetching data from the Internet, or loading local data from a database etc.
In this sample you can just add a constructor to the LearnViewModel and call DoAddTodoItem a couple of times:
public class LearnViewModel : BaseViewModel
{
public LearnViewModel()
{
DoAddTodoItem();
DoAddTodoItem();
}
...
This should show you something like this when you launch the app:

Binding ViewModel property to a View

I have the following (a base class for pages and a viewmodel class):
public class MySuperPage : Page {
public MySuperPageViewModel VM = new MySuperPageViewModel();
..........
..........
public class MySuperPageViewModel {
protected bool _ShowProgress = false;
public bool ShowProgress {
get { return _ShowProgress; }
set {
_ShowProgress = value;
OnPropertyChanged("ShowProgress");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string property) {
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
Then an actual page
public class MyPage : MySuperPage(){
public MyPage() : base() {
this.InitializeComponent();
}
}
The XAML is the following:
<my:MySuperPage
xmlns:my="using:MyNamespace"
x:Name="pageRoot"
x:Class="MyPages.MyPage"
DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"
....>
<ProgressRing IsActive="{Binding VM.ShowProgress, ElementName=pageRoot}" ... />
If in the code-behind I do actions such as
this.VM.ShowProgress = true; // or false
the effects are not visible.
Instead, things work if I assign object 'VM' to DefaultViewModel (which is an ObservableCollection):
this.DefaultViewModel["VM"] = VM;
I.e., in this last case using {Binding DefaultViewModel.VM.ShowProgress, ElementName=pageRoot} I manage to have the progress ring reflect the state of the VM instance.
I feel to miss something.
Your ViewModel in your page must be implemented as a property to make the binding work.
Change
public MySuperPageViewModel VM = new MySuperPageViewModel();
to
private MySuperPageViewModel _vm = new MySuperPageViewModel();
public MySuperPageViewModel VM { get { return _vm; }}

WPF MVVM two-way updates

I'm trying to setup a working two-way update by using this example.
These are the relevant code snippets:
XAML:
<Button Click="clkInit">Initialize</Button>
<Button Click="clkStudent">Add student</Button>
<Button Click="clkChangeStudent">Change students</Button>
(...)
<TabControl Name="tabControl1" ItemsSource="{Binding StudentViewModels}" >
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=StudentFirstName}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<Grid>
<Label Content="First Name" Name="label1" />
<TextBox Name="textBoxFirstName" Text="{Binding Path=StudentFirstName}" />
<Label Content="Last Name" Name="label2" />
<TextBox Name="textBoxLastName" Text ="{Binding Path=StudentLastName}" />
</Grid>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
Main Window:
public partial class MainWindow : Window
{
internal MainWindowViewModel myMWVM;
public MainWindow()
{
InitializeComponent();
}
private void clkInit(object sender, RoutedEventArgs e)
{
myMWVM= new MainWindowViewModel();
DataContext = myMWVM;
}
private void clkStudent(object sender, RoutedEventArgs e)
{
myMWVM.StudentViewModels.Add(new StudentViewModel());
}
// For testing - call a function out of the student class to make changes there
private void clkChangeStudent(object sender, RoutedEventArgs e)
{
for (Int32 i = 0; i < test.StudentViewModels.Count; i++)
{
myMWVM.StudentViewModels.ElementAt((int)i).changeStudent();
}
}
}
Main view:
class MainWindowViewModel : INotifyPropertyChanged
{
ObservableCollection<StudentViewModel> _studentViewModels =
new ObservableCollection<StudentViewModel>();
// Collection for WPF.
public ObservableCollection<StudentViewModel> StudentViewModels
{
get { return _studentViewModels; }
}
// Constructor. Add two stude
public MainWindowViewModel()
{
_studentViewModels.Add(new StudentViewModel());
_studentViewModels.Add(new StudentViewModel());
}
// Property change.
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Student view:
class StudentViewModel : INotifyPropertyChanged
{
Lazy<Student> _model;
string _studentFirstName;
public string StudentFirstName
{
get { return _studentFirstName; }
set
{
if (_studentFirstName != value)
{
_studentFirstName = value;
_model.Value.StudentFirstName = value;
OnPropertyChanged("StudentFirstName");
}
}
}
string _studentLastName;
public string StudentLastName
{
get { return _studentLastName; }
set
{
if (_studentLastName != value)
{
_studentLastName = value;
_model.Value.StudentLastName = value;
OnPropertyChanged("StudentLastName");
}
}
}
public void changeStudent()
{
_model.Value.changeStudent();
}
public StudentViewModel()
{
_studentFirstName = "Default";
_model = new Lazy<Student>(() => new Student());
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
THE student:
class Student
{
public string StudentFirstName { get; set; }
public string StudentLastName { get; set; }
public Student()
{
MessageBox.Show("Student constructor called");
}
public Student(string nm)
{
StudentLastName = nm;
}
public void changeStudent()
{
StudentLastName = "McDonald";
}
}
If you read until here I already thank you :) Still, by calling "clkChangeStudent" I don't see the changes in the textbox. I guess it's because I don't call the set-method of the StudentViewModel. The project I'm working on is a bit complex and a lot of things happen in the class (here Student) itself.
How can I get a textbox update by settings values in the Student-class itself?
Your actual code clearly won't notify changes to the interface. The reason is simple. Your method that changes the student name is in the Student model and that model does not implement the INotifyPropertyChanged.
There is 2 solutions to fix this issue depending on one question, does the changeStudent() method has to stick with the object model, that is to say, can your requirements allows you to move the changeStudent() method to the view model?
If yes then, first solution, simply remove the changeStudent method from the model and move it to the view model like this:
class StudentViewModel : INotifyPropertyChanged
{
...
public void changeStudent()
{
this.StudentLastName = "McDonald";
}
}
In the other case, second solution, you have to raise events whenever a model property changes and then get your view model to suscribe to these changes. You can proceed like this in the model:
class Student : INotifyPropertyChanged
{
...
private string studentLastName;
public string StudentLastName
{
get
{
return this.studentLastName;
}
set
{
if(this.studentLastname != value)
{
this.studentLastName = value;
this.OnPropertyChanged("StudentLastName");
}
}
}
}
And for the view model:
class StudentViewModel : INotifyPropertyChanged
{
...
public StudentViewModel(Student model)
{
this._model = model;
this._model.PropertyChanged += (sender, e) =>
{
if(e.PropertyName == "StudentLastName")
{
this.OnPropertyChanged("StudentLastName");
}
};
}
}
Both solution will work. It is really import that you understand that your code explicitely needs to notifies the interface whenever a value changes.
ChangeStudent doesn't call any of the methods that trigger a property notify event in the view model, it alters the underlying model instead. It's these events that trigger the view to update itself.
As an aside you should also look at command binding from the view instead of using click handlers in the code-behind. That way your view doesn't need to know anything about the view model that's attached and can be pure presentation.
First you should use commands instead of events.
In your current structure you have to add an
OnPropertyChanged("StudentLastName");
call to your ChangedStudent() Method in StudentViewModel.
After that you have to set the UpdateSourceTrigger of the Bindings to PropertyChanged
Text="{Binding Path=StudentFirstName, UpdateSourceTrigger=PropertyChanged}"

Categories

Resources