I am currently doing a project with the MVVM method in NET MAUI to add, modify and delete drivers.
I have a template that contains the name, first name and number of points of the driver.
Then I have two views each with a model view:
- One that represents the list of my drivers with the possibility to add a driver, to select a driver from the list by going to another page (PageListPilotViewModel).
- And another one which represents the selected driver in another page to be able to modify its data and the possibility of removing it. (ProfilePilotViewModel)
At the moment I can select, add the driver and modify the driver in the other page. But I can't delete the driver in the profile page.
Here is what I have done so far:
-> Models : Pilote Model
public class PiloteModel : INotifyPropertyChanged
{
private string _nom;
public string Nom
{
get { return _nom; }
set { _nom = value; OnPropertyChanged(); }
}
private string _prenom;
public string Prenom
{
get { return _prenom; }
set { _prenom = value; OnPropertyChanged(); }
}
private int _points;
public int Points
{
get { return _points; }
set { _points = value; OnPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
-> View : ProfilPilotePage
<vm:PageListPiloteViewModel></vm:PageListPiloteViewModel>
</ContentPage.BindingContext>
<VerticalStackLayout>
<StackLayout>
<Entry Text="{Binding Pilote.Nom, Mode=TwoWay}" Placeholder="{Binding Nom}"></Entry>
<Entry Text="{Binding Pilote.Prenom}" Placeholder="{Binding Pilote.Prenom}"></Entry>
<Entry Text="{Binding Pilote.Points}" Placeholder="{Binding Pilote.Points}"></Entry>
<Button Command="{Binding OnsupprimerPiloteCommand}">
</Button>
</StackLayout>
-> code behind the profilePilotPage view
public partial class ProfilPilotePage : ContentPage
{
private PageListPiloteViewModel _viewModel;
public ProfilPilotePage(PageListPiloteViewModel viewModel)
{
InitializeComponent();
_viewModel = viewModel;
_viewModel.SupprimerPiloteClicked += OnSupprimerPiloteClicked;
BindingContext = _viewModel;
}
private void OnSupprimerPiloteClicked(object sender, PiloteModel e)
{
_viewModel.ListePilotes.Remove(e);
}
-> model views : PageListPilotViewModel , to be able to delete also the driver in the list
public ICommand OnsupprimerPiloteCommand { get; set; }
public PageListPiloteViewModel()
{
ValiderCommand = new Command(AjouterPilote);
OnsupprimerPiloteCommand = new Command(OnSupprimerPiloteClicked);
SelectedPilote = new PiloteModel();
ListePilotes = new ObservableCollection<Models.PiloteModel>();
ListePilotes.Add(new Models.PiloteModel { Nom = "Fabio", Prenom = "Quartaro", Points = 215 });
}
private void OnSupprimerPiloteClicked()
{
SupprimerPiloteClicked?.Invoke(this, SelectedPilote);
}
->code behind the PageListPiloteView: with the error I encounter on the last :
await Navigation.PushAsync(new ProfilePilotPage{ BindingContext = viewModel }) :
CS7036 Error None of the specified arguments match the 'viewModel' mandatory parameter of 'ProfilePilotPage.ProfilePilotPage(PageListPilotViewModel)'
private async void SelectionnerPilote(object sender, SelectionChangedEventArgs e)
{
PiloteModel selectedPilote = (PiloteModel)((CollectionView)sender).SelectedItem;
ProfilPiloteViewModel viewModel = new ProfilPiloteViewModel();
viewModel.Pilote = selectedPilote;
await Navigation.PushAsync(new ProfilPilotePage{ BindingContext = viewModel });
}
}
Do you have any idea how to make the specified arguments mandatory please ?
You've mixed up constructor and initializer.
This line
await Navigation.PushAsync(new ProfilPilotePage{ BindingContext = viewModel });
should be
await Navigation.PushAsync(new ProfilPilotePage(viewModel));
The reason for this is that you're defining an argument in the signature of the ProfilPilotePage's constructor:
public ProfilPilotePage(PageListPiloteViewModel viewModel)
{
//...
}
Therefore, you must pass the ViewModel argument.
At first, you can try to use the ewerspej's solution or only add a default construction method without any parameter into the ProfilPilotePage to fix the error caused by await Navigation.PushAsync(new ProfilePilotPage{ BindingContext = viewModel }) . Such as:
public partial class ProfilPilotePage : ContentPage
{
private PageListPiloteViewModel _viewModel;
public ProfilPilotePage()
{
InitializeComponent();
}
public ProfilPilotePage(PageListPiloteViewModel viewModel)
{
InitializeComponent();
_viewModel = viewModel;
_viewModel.SupprimerPiloteClicked += OnSupprimerPiloteClicked;
BindingContext = _viewModel;
}
}
And then I saw you used both the mvvm and the code behind. You can remove the OnSupprimerPiloteClicked(object sender, PiloteModel e) in the page.cs and change the OnSupprimerPiloteClicked() in the view model. Such as:
private void OnSupprimerPiloteClicked()
{
ListePilotes.Remove(SelectedPilote);
}
Finally, I saw SelectedPilote = new PiloteModel(); in your viewmodel. Which item in the list did you want to delete? I think it should be the seleted item not a new PiloteModel().
Related
I am a beginner at Xamarin and I am trying to pass value from one page to another using QueryProperty, but I keep getting null values.
Here is the Page where the value comes from:
<StackLayout>
<Button Text="Pass" Command="{Binding passCommand}"></Button>
</StackLayout>
The code behind:
public Page()
{
InitializeComponent();
passCommand = new Command(passFunc);
BindingContext = this;
}
public ICommand passCommand { get; }
private async void passFunc()
{
string str = "Hello";
await Shell.Current.GoToAsync($"{nameof(Page3)}?str={str}");
}
And here is the receiving page:
<StackLayout>
<Label Text="{Binding str}"/>
</StackLayout>
The code behind:
[QueryProperty(nameof(str), nameof(str))]
public partial class Page3 : ContentPage
{
public Page3()
{
InitializeComponent();
BindingContext = this;
showdisp();
}
public string str { set; get; }
public async void showdisp()
{
await App.Current.MainPage.DisplayAlert("Hello", str, "OK");
}
}
The passed value should be put in the Label and the popup display alert. When I tried to put breakpoints, str value is still null. Navigating between pages are fine.
Can someone point out if where the error is T_T
Thanks in advance.
Your property "str" needs to raise a PropertyChanged event, so that the binding updates the value:
[QueryProperty(nameof(str), nameof(str))]
public partial class Page3 : ContentPage
{
public Page3()
{
InitializeComponent();
BindingContext = this;
// attention: this won't show the passed value,
// because QueryProperty values only are set after construction
//showdisp();
}
private string _str;
public string str
{
get => _str;
set
{
if(_str == value) return;
_str = value;
// Let the bound views know that something changed, so that they get updated
OnPropertyChanged();
// optional, call showdisp() when value changed
showdisp();
}
}
public async void showdisp()
{
await App.Current.MainPage.DisplayAlert("Hello", str, "OK");
}
}
However, since the parameter only is set after construction of Page3 finished, your showdisp() method won't have the correct value. You need to call it later.
You should also consider using a ViewModel and apply MVVM.
To pass data beetwen viewmodels when navigating I use query parameters (IQueryAttributable), i.e.:
NavigationParameters[nameof(SomeProperty)] = SomeProperty;
await Shell.Current.GoToAsync("SomePage", NavigationParameters);
It works as it should be working, but I wish to put SomePage into a TabBar:
<TabBar>
<ShellContent Route="SomePage"
ContentTemplate="{DataTemplate local:SomePage}"/>
...
</TabBar>
Is there a way to pass data when user click/tap SomePage icon on the tab bar? Is there some event for that so I could hook up GoToAsync method? Or maybe there is another way than query to pass data beetwen viewmodels?
MauiProgram.cs:
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
builder.Services
.AddSingleton<Page_1>
.AddSingleton<ViewModel_1>
.AddSingleton<Page_2>
.AddSingleton<ViewModel_2>
.AddSingleton<ISharedDataInterface, SharedDataInterfaceImplementation>();
return builder.Build();
}
}
Page_1.xaml.cs:
public partial class Page_1 : ContentPage
{
public LogoPage(ViewModel_1 viewModel_1)
{
BindingContext = viewModel_1;
InitializeComponent();
}
}
Page_2.xaml.cs:
public partial class Page_2 : ContentPage
{
public LogoPage(ViewModel_2 viewModel_2)
{
BindingContext = viewModel_2;
InitializeComponent();
}
}
ViewModel_1.cs:
public class ViewModel_1
{
public ViewModel_1(ISharedDataInterface sharedDataInterfaceImplementation)
{
...
}
}
ViewModel_2.cs:
public class ViewModel_2
{
public ViewModel_2(ISharedDataInterface sharedDataInterfaceImplementation)
{
...
}
}
If the two pages are in the same Tab,you can create a static global variable for the viewmodel in App.xaml.cs and access this variable in your different pages.
For example,we can first define a view model (e.g. TestViewModel.cs):
public class TestViewModel: INotifyPropertyChanged
{
public TestViewModel() {
str = "123";
}
public string _str;
public string str
{
get
{
return _str;
}
set
{
_str = value;
OnPropertyChanged("str");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
And define a static global variable in App.xaml.cs:
public partial class App : Application
{
public static TestViewModel testViewModel;
public App()
{
InitializeComponent();
MainPage = new AppShell();
testViewModel = new TestViewModel();
}
}
Then , you can assgin the global variable for BindingContext in different pages:
public partial class Tab1 : ContentPage
{
public Tab1()
{
InitializeComponent();
BindingContext = App.testViewModel;
}
}
public partial class Tab2 : ContentPage
{
public Tab2()
{
InitializeComponent();
BindingContext = App.testViewModel;
}
private void Button_Clicked(object sender, EventArgs e)
{
App.testViewModel.str = "Test";
}
}
"You could also hold handles to other view models in your view model, like a common section that's shared between all your pages."
That is the advice to follow.
Suppose each tab's viewmodel has public MySharedType Shared { get; private set; },
and constructor is MyTab1ViewModel(MySharedType shared) { this.Shared = shared; }.
Then from xaml, access a shared property by: {Binding Shared.MyProperty1}.
Let's see a practical example.
You have one TabbedPage and two ContentPage. The idea is that you will pass the correct parameter when the user taps the specific tab. In this context you can pass any parameter on demand to your tab page.
<?xml version="1.0" encoding="utf-8" ?>
<TabbedPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyApp.Pages.TabPage"
xmlns:pages="clr-namespace:MyApp.Pages">
<pages:Page1/>
<pages:Page2/>
</TabbedPage>
Add the bellow code to the TabbedPage code behind.
public TabPage()
{
InitializeComponent();
this.CurrentPageChanged += TabPage_CurrentPageChanged;
PassParams();
}
private void TabPage_CurrentPageChanged(object sender, EventArgs e)
{
PassParams();
}
private void PassParams()
{
if (this.CurrentPage.GetType() == typeof(Page1))
{
Page1 cur = (Page1)this.CurrentPage;
cur.Param1 = 123;
}
if (this.CurrentPage.GetType() == typeof(Page2))
{
Page2 cur = (Page2)this.CurrentPage;
cur.Param2 = 456;
}
}
Add the below to Page1 code behind. About the same code goes to Page2, just replace number 1.
public Page1()
{
InitializeComponent();
}
int param1;
public int Param1
{
set
{
param1 = value;
OnPropertyChanged();
BindingContext = new Page1ViewModel(param1);
}
}
The Page1ViewModel will have to handle the parameter.
public Page1ViewModel(int param1)
{
//populate your view with your models
}
I am tying to further understand MVVM with some example scenario. I have a rootpage with a 'maindisplay' textblock. I would like to display 'status' or 'scenarios' from activation of any form of UI eg. togglebutton on the 'maindisplay' textblock.
I am able to bind the the page navigation info in the rootpageviewmodel to the textblock. However, I am not able to achieve the result when displaying info from different page.
I have checked another post multiple-viewmodels-in-same-view & Accessing a property in one ViewModel from another it's quite similar but it didn't work.
Please help. Thanks.
While accessing the RootPageViewModel should retain the instance?
View
<TextBlock Text="{x:Bind RootViewModel.MainStatusContent, Mode=OneWay}"/>
RootPage.xaml.cs
public sealed partial class RootPage : Page
{
private static RootPage instance;
public RootPageViewModel RootViewModel { get; set; }
public RootPage()
{
RootViewModel = new RootPageViewModel();
this.InitializeComponent();
// Always use the cached page
this.NavigationCacheMode = NavigationCacheMode.Required;
}
public static RootPage Instance
{
get
{
if (instance == null)
{
instance = new RootPage();
}
return instance;
}
}
private void nvTopLevelNav_ItemInvoked(NavigationView sender, NavigationViewItemInvokedEventArgs args)
{
if (args.IsSettingsInvoked)
{
contentFrame.Navigate(typeof(SettingsPage));
RootViewModel.MainStatusContent = "Settings_Page";
}
else
{
var navItemTag = args.InvokedItemContainer.Tag.ToString();
RootViewModel.MainStatusContent = navItemTag;
switch (navItemTag)
{
case "Home_Page":
contentFrame.Navigate(typeof(HomePage));
break;
case "Message_Page":
contentFrame.Navigate(typeof(MessagePage));
break;
}
}
}
}
RootPage ViewModel:
public class RootPageViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private static RootPageViewModel instance = new RootPageViewModel();
public static RootPageViewModel Instance
{
get
{
if (instance == null)
instance = new RootPageViewModel();
return instance;
}
}
public RootPageViewModel()
{
}
private string _mainStatusContent;
public string MainStatusContent
{
get
{
return _mainStatusContent;
}
set
{
_mainStatusContent = value;
OnPropertyChanged();
}
}
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
MessagePage.xaml.cs - to access RootPage ViewModel
public sealed partial class MessagePage : Page
{
public MessagePageViewModel MessageViewModel { get; set; }
public MessagePage()
{
MessageViewModel = new MessagePageViewModel();
this.InitializeComponent();
// Always use the cached page
this.NavigationCacheMode = NavigationCacheMode.Required;
}
private void Message1_Checked(object sender, RoutedEventArgs e)
{
RootPageViewModel.Instance.MainStatusContent = "Message 1 Selected";
}
private void Message1_Unchecked(object sender, RoutedEventArgs e)
{
RootPageViewModel.Instance.MainStatusContent = "Message 1 De-Selected";
}
}
When I debug the value did write to the instance but did't update the TextBlock. Did I do anything wrong in my XAML binding?
UWP C# MVVM How To Access ViewModel from Other Page
The better way is make static variable for RootPage, but not make singleton instance for RootPage and RootPageViewModel.
For example:
public RootPage ()
{
this.InitializeComponent();
this.NavigationCacheMode = NavigationCacheMode.Required;
Instance = this;
RootViewModel = new RootPageViewModel();
}
public static RootPage Instance;
Usage
private void Message1_Checked(object sender, RoutedEventArgs e)
{
RootPage.Instance.RootViewModel.MainStatusContent = "Message 1 Selected";
}
private void Message1_Unchecked(object sender, RoutedEventArgs e)
{
RootPage.Instance.RootViewModel.MainStatusContent = "Message 1 De-Selected";
}
I have WPF application with Combobox and Button. After I enter value into textbox and press button, the value from textbox should appear in updated list of combobox. I am trying to achieve this with MVVM and binding to combobox. Here is part of code from ViewModel.
public class ViewModel:INotifyPropertyChanged
{
DomainLogic dl = new DomainLogic();
public event PropertyChangedEventHandler PropertyChanged;
private ObservableCollection<string> expenseCategories = new ObservableCollection<string>();
public ObservableCollection<string> ExpenseCategories
{
get
{
return expenseCategories;
}
set
{
expenseCategories = value;
OnPropertyChanged("ExpenseCategories");
}
}
public ViewModel()
{
expenseCategories = new ObservableCollection<string>(dl.GetExpenseCategories());
}
private void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
Also I am using EF to access DB and DomainLogic class has a method to list all Expense Categories.
Here is code-behind from window:
DomainLogic dl = new DomainLogic();
public Window1()
{
InitializeComponent();
DataContext = new ViewModel();
}
private void button1_Click(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrWhiteSpace(textBox.Text))
{
dl.CreateNewExpenseCategory(textBox.Text);
}
else
{
MessageBox.Show("Enter category!");
}
}
Here is also XAML:
<ComboBox x:Name="ExpCategory" HorizontalAlignment="Left" Margin="72,50,0,0" VerticalAlignment="Top" Width="130" ItemsSource="{Binding ExpenseCategories, UpdateSourceTrigger=PropertyChanged}" />
When I add the new category the combobox isn't updated. I'm new to the whole MVVM pattern and I think I'm missing something here.
//EDIT
public void CreateNewExpenseCategory(string name)
{
using (var context = new ExpenseEntities())
{
ExpenseCategory category = new ExpenseCategory() { CategoryName = name};
context.ExpenseCategory.Add(category);
context.SaveChanges();
}
}
The problem is that CollectionChanged event doesn't fire.
You are adding a new element inside your DataContext, but you aren't updating your local view of data.
Once you update the DataContext you should refresh your ObservableCollection or use Local.
Here how you could use Local:
public ViewModel()
{
expenseCategories = dl.GetExpenseCategories().Local;
}
So you can directly do:
expenseCategories.Add(new ExpenseCategory() {textBox.Text});
dl.GetContext().SaveChanges();
Or you have to update the ObservableCollection:
dl.CreateNewExpenseCategory(textBox.Text);
// Update your ViewModel ObservableCollection.
However i think that you should use a Command and not an Event so you can update the ObservableCollection directly inside the ViewModel.
Example:
using Prism.Commands;
//Other usings
public class ViewModel : INotifyPropertyChanged
{
// Your class methods and properties
public DelegateCommand<string> AddNewExpenseCategory
{
get
{
return new DelegateCommand<string>(Execute_AddNewExpenseCategory);
}
}
public void Execute_AddNewExpenseCategory(string param)
{
expenseCategories.Add(new ExpenseCategory() { param });
dl.GetContext().SaveChanges();
}
I am trying to get the content of a TextBox updated using Binding in a MVVM environment. When a Button receive focus, it passes a value, and that value should be reflected in the TextBox. I seem to have the first part right, however seems to be struggling at passing the value..
I know the question about MVVM has been asked before (including by myself), but I really cannot get it, for some reasons..
So I start with my model:
public class iText : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _text;
public string Text
{
get { return _text; }
set
{
_text = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Text)));
}
}
I then continue with my ViewModel:
private iText _helper = new iText();
public iText Helper
{
get { return _helper; }
set
{
_helper = value;
}
}
The XAML page:
<Page.Resources>
<scan:ModelDataContext x:Key="ModelDataContext" x:Name="ModelDataContext"/>
</Page.Resources>
<TextBox Text="{Binding Helper.Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
I then try to update the Text from MainPage.cs
public sealed partial class MainPage : Page
{
public MainPageViewModel iText { get; set; }
public MainPage()
{
InitializeComponent();
iText = new MainPageViewModel();
}
private void btn_GotFocus(object sender, RoutedEventArgs e)
{
var str = "test"
iText.Helper.Text = str;
}
I could really appreciate if someone could tell me what I do wrong, and where. Thanks so much in advance.
In your MainPage constructor, try setting the datacontext to your ViewModel.
Something like...
public MainPage()
{
InitializeComponent();
iText = new MainPageViewModel();
this.dataContext = iText;
}