This is the view model file:
public partial class LoginViewModel : ObservableObject
{
void Login()
{
Navigation.PushAsync(new HomePage()); // <== The name `Navigation` does not exist in the current context
}
}
Why Navigation can't be accessed in view model files, but can be accessed in .xaml.cs files?
If you are using shell, you can check document .NET MAUI Shell navigation.
Shell includes a URI-based navigation experience that uses routes to navigate to any page in the app, without having to follow a set navigation hierarchy. In addition, it also provides the ability to navigate backwards without having to visit all of the pages on the navigation stack.
Of course, if you indeed need to navigate on your ViewModel, you can try to pass the Navigation through the VM Constructor. Since pages inherit from VisualElement, they directly inherit the Navigation property.
public class LoginViewModel
{
public INavigation Navigation { get; set; }
public ICommand ContinueBtnClicked { get; set; }
public LoginViewModel(INavigation navigation)
{
this.Navigation = navigation;
this.ContinueBtnClicked = new Command(async () => await GotoNextPage());
}
public async Task GotoNextPage()
{
await Navigation.PushAsync(new NextPage());
}
}
LoginPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MauiApp210.LoginPage"
Title="LoginPage">
<VerticalStackLayout>
<Label
Text="Welcome to .NET MAUI!"
VerticalOptions="Center"
HorizontalOptions="Center" />
<Button Text="Navigate" Command="{Binding ContinueBtnClicked}"/>
</VerticalStackLayout>
</ContentPage>
LoginPage.xaml.cs
public partial class LoginPage : ContentPage
{
public LoginPage()
{
InitializeComponent();
this.BindingContext = new LoginViewModel(Navigation);
}
}
Related
I am building a Xamarin Forms application using Shell in which I am not building your typical flyout. I have created a template each for the header, footer and content. The content contains data which is fetched from a database.
I'm defining these in my app.xaml file.
<Style TargetType="Shell" ApplyToDerivedTypes="True">
<Setter Property="FlyoutFooterTemplate" Value="{DataTemplate common:FlyoutFooterTemplate}"/>
<Setter Property="FlyoutHeaderTemplate" Value="{DataTemplate common:FlyoutHeaderTemplate}"/>
<Setter Property="FlyoutContentTemplate" Value="{DataTemplate common:FlyoutContentTemplate}"/>
</Style>
I have created the FlyoutContentTemplate as a RefreshView.
<?xml version="1.0" encoding="utf-8" ?>
<RefreshView
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:InFlight.ViewModels.Common"
xmlns:model="clr-namespace:InFlight.Core.Models;assembly=InFlight.Core"
x:Class="InFlight.Views.Common.FlyoutContentTemplate"
x:DataType="local:FlyoutContentTemplateViewModel"
Command="{Binding LoadFlightsCommand}"
IsRefreshing="{Binding IsBusy, Mode=TwoWay}">
<CollectionView
ItemsSource="{Binding Flights}"
SelectionMode="None">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout Padding="10" x:DataType="model:Flight">
<Label Text="{Binding CallSign}"
LineBreakMode="NoWrap"
Style="{StaticResource LabelMedium}" />
<Label Text="{Binding FlightNotes}"
LineBreakMode="NoWrap"
Style="{StaticResource LabelSmall}" />
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
<CollectionView.Footer>
<Button Command="{Binding AddFlightCommand}" Text="Add Flight" />
</CollectionView.Footer>
</CollectionView>
</RefreshView>
The code behind simply sets the BindingContext.
using InFlight.ViewModels.Common;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace InFlight.Views.Common
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class FlyoutContentTemplate : RefreshView
{
readonly FlyoutContentTemplateViewModel viewModel;
public FlyoutContentTemplate()
{
InitializeComponent();
this.BindingContext = viewModel = new FlyoutContentTemplateViewModel();
}
}
}
The view model is fairly simple and handles the LoadFlightsCommand triggered by the RefreshView and the navigation to the AddEditFlightPage.
using InFlight.Core.Models;
using InFlight.Core.Respositories;
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Threading.Tasks;
using Xamarin.Forms;
namespace InFlight.ViewModels.Common
{
public class FlyoutContentTemplateViewModel : BaseViewModel
{
public ObservableCollection<Flight> Flights { get; set; }
public Command LoadFlightsCommand { get; }
public Command AddFlightCommand { get; }
readonly IFlightRepository flightRepository;
public FlyoutContentTemplateViewModel()
{
flightRepository = DependencyService.Get<IFlightRepository>();
Flights = new ObservableCollection<Flight>();
LoadFlightsCommand = new Command(ExecuteLoadFlightsCommand);
AddFlightCommand = new Command(async () => await OnAddFlightCommand());
ExecuteLoadFlightsCommand();
}
private async Task OnAddFlightCommand()
{
await Shell.Current.GoToAsync("AddEditFlightPage");
Shell.Current.FlyoutIsPresented = false;
}
private void ExecuteLoadFlightsCommand()
{
IsBusy = true;
try
{
Flights.Clear();
var flts = flightRepository.GetFlights();
foreach (var flight in flts)
{
Flights.Add(flight);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
finally
{
IsBusy = false;
}
}
}
}
This all seems to work well, but at the moment I need to pull to refresh in order to trigger the LoadFlightsCommand.
The issue is that I want to trigger the data refresh when navigating back from my add page. I've seen posts where people tap into the OnAppearing event in order to trigger the refresh, but as I am using a template and refresh view, I don't see how I can do that.
Have I maybe taken the wrong approach and using Shell for a purpose it shouldn't be used for?
I'm thinking that the solution probably involves the events of the shell itself?
Any advice would be much appreciated.
First Approach
I don't have the full picture on how you are defining your Shell's routes/navigation, but I believe what you are trying to achieve could be done using Shell event OnNavigated or OnNavigating.
First thing, Shell (AppShell.xaml.cs) need to have access to the instance of your FlyoutContentTemplateViewModel in roder to call the method ExecuteLoadFlightsCommand() from there.
Turn the accessibility of ExecuteLoadFlightsCommand() to public.
AppShell.xaml.cs
public FlyoutContentTemplateViewModel flyoutContentTemplateViewModel;
public AppShell()
{
flyoutContentTemplateViewModel = new();
InitializeComponent();
}
protected override void OnNavigated(ShellNavigatedEventArgs args)
{
var previousRouteString = args?.Previous?.Location?.OriginalString;
var currentRouteString = args?.Current?.Location?.OriginalString;
if (previousRouteString != null && previousRouteString.Contains("[DEPENDS ON YOUR ROUTES NAME]") &&
currentRouteString.Contains("[DEPENDS ON YOUR ROUTES NAME]"))
{
flyoutContentTemplate.ExecuteLoadFlightsCommand();
}
base.OnNavigated(args);
}
In your FlyoutContentTemplate(), use the same ViewModel instance from the field that we have added in your AppShell.
public FlyoutContentTemplate()
{
InitializeComponent();
BindingContext = viewModel = (Shell.Current as AppShell).flyoutContentTemplateViewModel;
}
Second Approach
If you don't want to store your VM in your AppShell then you might use DependencyService.
Extract an interface from your FlyoutContentTemplateViewModel: on visual studio select the class name, right click, in the menu click "Quick Actions and refactoring", after that click "Extract interface", VS will generate an interface called IFlyoutContentTemplateViewModel:
public interface IFlyoutContentTemplateViewModel
{
Command AddFlightCommand { get; }
ObservableCollection<Flight> Flights { get; set; }
bool IsBusy { get; }
Command LoadFlightsCommand { get; }
Task OnAddFlightCommand()
void ExecuteLoadFlightsCommand();
}
FlyoutContentTemplate.xaml.cs
public FlyoutContentTemplate()
{
InitializeComponent();
BindingContext = viewModel = new FlyoutContentTemplateViewModel();
DependencyService.RegisterSingleton<IFlyoutContentTemplateViewModel>(viewModel);
}
AppShell.xaml.cs
...
if (previousRouteString != null && previousRouteString.Contains("[DEPENDS ON YOUR ROUTES NAME]") &&
currentRouteString.Contains("[DEPENDS ON YOUR ROUTES NAME]"))
{
DependencyService.Resolve<IFlyoutContentTemplateViewModel>()?.ExecuteLoadFlightsCommand();
}
Third Approach
Calling ExecuteLoadFlightsCommand() from OnDisappearing() of AddEditFlightPage, instead of AppShell.OnNavigated().
AddEditFlightPage.xaml.cs
protected override void OnDisappearing()
{
(DependencyService.Resolve<IFlyoutContentTemplateViewModel>())?.ExecuteLoadFlightsCommand();
base.OnDisappearing();
}
I'm trying to bind a property to a view model.
I get the following error:
Error XFC0009 No property, BindableProperty, or event found for "ViewModel", or mismatching type between value and property.
public abstract class BaseTestView : ContentView
{
public BaseVm ViewModel
{
get => (BaseVm)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, BindingContext = value);
}
public static BindableProperty ViewModelProperty { get; set; } = BindableProperty.Create(nameof(ViewModel), typeof(BaseVm), typeof(BaseTestView));
}
<v:BaseTestView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:MyProject.ViewModels"
xmlns:v="clr-namespace:MyProject.Views"
x:Class="MyProject.Views.ChildTestView"
x:DataType="vm:ChildTestVm">
<v:BaseTestView.Content>
<StackLayout>
<Label Text="{Binding Foo}" />
</StackLayout>
</v:BaseTestView.Content>
</v:BaseTestView>
public partial class ChildTestView : BaseTestView
{
public ChildTestView() : base()
{
InitializeComponent();
}
}
public class ChildTestVm : BaseVm
{
public string Foo { get; set; }
public ChildTestVm()
{
Title = "Test";
Foo = "some stuff";
}
}
public class HomeVm : BaseVm
{
public ChildTestVm Tested { get; set; }
}
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:MyProject.ViewModels"
xmlns:v="clr-namespace:MyProject.Views"
x:Class="MyProject.Pages.HomePage"
x:DataType="HomeVm">
<ContentPage.Content>
<StackLayout>
<v:ChildTestView ViewModel="{Binding Tested}" />
<!-- ^ Error here /-->
</StackLayout>
</ContentPage.Content>
</ContentPage>
public partial class HomePage : ContentPage
{
}
Any idea of what this error means and how to fix it?
I tried some experiments, but failed to figure out why it gave that complaint - every variation I tried also gave that error.
Instead, do it this way:
First, set the BindingContext of ChildTestView:
<v:ChildTestView BindingContext="{Binding Tested}" />
That data-binds ChildTestView to the ChildTestVm from Tested.
If you also need access to the Vm for code behind, do it this way:
ChildTestView.xaml.cs:
private ChildTestVm ViewModel => (ChildTestVm)BindingContext;
Now in methods of ChildTestView, you can use ViewModel.Foo.
NOTE: If you dynamically change Tested:
If you have code anywhere that does Tested = ... AFTER HomePage is loaded and visible, then getting that to work requires Tested setter to do OnPropertyChanged(); (or other MVVM data binding mechanism). This is necessary to inform XAML of changes.
I have a login/register page, and I want to be able to navigate between them routing navigation (//LoginPage) like in a web app. I cant really make it work. I have built a AppShell.xaml page and i have registered my LoginPage and RegisterPage in App.xaml.cs (I have tried registering them in AppShell.xaml.cs too) and I have a viewmodel that has 2 functions for navigating between login and register pages but when I click on the button to navigate I get this
System.NullReferenceException: 'Object reference not set to an instance of an object.'
Here is my view-model
namespace Appointments.ViewModels
{
public class RegLogViewModel : BaseViewModel
{
public AsyncCommand GoToRegisterCommand { get; }
public AsyncCommand GoToLoginCommand { get; }
public RegLogViewModel()
{
GoToLoginCommand = new AsyncCommand(GoToLogin);
GoToRegisterCommand = new AsyncCommand(GoToRegister);
}
async Task GoToRegister()
{
await Shell.Current.GoToAsync($"//{ nameof(RegisterPage)}");
}
async Task GoToLogin()
{
await Shell.Current.GoToAsync($"//{ nameof(LoginPage)}");
}
}
My AppShell.xaml
<Shell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Appointments.AppShell"
xmlns:views="clr-namespace:Appointments.Views">
//// I HAVE TRIED WITH AND WITHOUT THIS SHELLCONTENT\\\\\
<ShellContent Route="LoginPage" ContentTemplate="{DataTemplate views:LoginPage}"/>
<ShellContent Route="RegisterPage" ContentTemplate="{DataTemplate views:RegisterPage}"/>
</Shell>
My AppShell.xaml.cs
namespace Appointments
{
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
}
}
}
My App.xaml.cs
namespace Appointments
{
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new LoginPage();
}
}
}
And Login.xaml
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:Appointments.ViewModels"
x:DataType="viewmodels:RegLogViewModel"
x:Class="Appointments.Views.LoginPage">
<ContentPage.Content>
<StackLayout>
<Label
Text="Login Page!"
FontSize="Title"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand" />
<Label
Text="{Binding Name}"/>
<Entry
Text="{Binding Name}"/>
<Button
Text="Go To Registration"
Command="{Binding GoToRegisterCommand}"/>
<ActivityIndicator IsRunning="{Binding IsBusy, Mode=OneWay}"/>
</StackLayout>
</ContentPage.Content>
</ContentPage>
The problem was that in App.xaml.cs I have
MainPage = new LoginPage();
but I had to use
MainPage = new AppShell();
it will work but now I don't understand how the app knows to open my LoginPage first and not RegisterPage or any other page
Provide BindingContext of the ViewModel to XAML page have benefits of IntelliSense. However, this syntax only works if the ViewModel has an empty contractor eg: MainPageViewModel()
<ContentPage.BindingContext>
<viewModels:MainPageViewModel />
</ContentPage.BindingContext>
If MainPageViewModel doesn't have empty constructor but only have parameters constructor eg: MainPageViewModel(param1, param2), then the above XAML syntax will throw compile error. How would I do this in XAML?
You could have a default constructor for your view model that would just serve IntelliSense purposes and then use the view model constructor with parameters in the page's constructor. That would give you the IntelliSense while being able to call the view models parameterized constructor. Something like this:
View model
public class SomeViewModel
{
public string SomeValue { get; set; }
//intellisense only constructor
public SomeViewModel(){
SomeValue = "test";
}
//real constructor
public SomeViewModel(string someValue){
SomeValue = someValue;
}
}
Page XAML
<Window.BindingContext>
<local:SomeViewModel />
</Window.BindingContext>
Page constructor
public SomePage()
{
this.BindingContext = new SomeViewModel("real value");
}
After looking at WeeklyXamarin.Mobile Repo. I found that this can be implemented nicely with the following:
Create a PageBase as follow:
public class PageBase : ContentPage { }
public class PageBase<TViewModel> : PageBase
{
public TViewModel ViewModel { get; set; }
public PageBase()
{
BindingContext = ViewModel = Container.Instance.ServiceProvider.GetRequiredService<TViewModel>();
}
}
Then the ViewModel in XAML can be passed in with x:TypeArguments like:
<views:PageBase
x:Class="WeeklyXamarin.Mobile.Views.AboutPage"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewmodels="clr-namespace:WeeklyXamarin.Core.ViewModels;assembly=WeeklyXamarin.Core"
xmlns:views="clr-namespace:WeeklyXamarin.Mobile.Views"
xmlns:vm="clr-namespace:WeeklyXamarin.Core.ViewModels;assembly=WeeklyXamarin.Core"
Title="{Binding Title}"
x:TypeArguments="viewmodels:AboutViewModel"
mc:Ignorable="d">
AboutViewModel has a parameter in the contractor and work nicely with the app
public AboutViewModel(INavigationService navigation, IDataStore dataStore) : base(navigation)
Credit: Mr Kym Phillpotts for his code at WeeklyXamarin repo
I think you could not do that in xaml. There is no such syntax in Xamarin.forms. The default usage is the constructor with no parameters. Make a simple example for your reference.
ViewModel:
public class viewModels
{
public string text1 { get; set; }
public viewModels()
{
text1 = "text1";
}
}
BindingContext in xaml:
<StackLayout>
<Label
HorizontalOptions="CenterAndExpand"
Text="{Binding text1}"
VerticalOptions="CenterAndExpand">
<Label.BindingContext>
<local:viewModels />
</Label.BindingContext>
</Label>
</StackLayout>
I have been searching everywhere on how to implement this IOS platform specific UI component (https://components.xamarin.com/view/flyoutnavigation) in my Xamarin.Forms PCL project, but I have not been able to understand how this would be possible.
I have come upon multiple "Buzz-Words" which I may be able to use, but I am still too new to fully understand what they mean and how I will be able to use them:
Custom Renderers:
With this, I understand that one can customize components available in Xamarin.Forms and create export assemblies in order to "push" platform specific code through to these components from their respective platforms.
Dependency Injection:
With this, I understand that one can create classes, and in the constructor method of those classes, pass through objects that will allow us to incorporate platform-specific code. (How? I have no idea...)
Xamarin.Forms DependencyService:
With this, I understand that we can somehow integrate platform specific code from the shared code (from the portable library class)
Please, I have so many gaps in my knowledge and I am trying so very hard to understand, but I just can't wrap my head around it!
Thanks in advance for your help.
First of All create a xaml page with .cs and give the name as "MenuMasterPage" xaml code
<?xml version="1.0" encoding="UTF-8"?>
<MasterDetailPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="TestAppForms.Pages.MenuMasterPage">
<MasterDetailPage.Master>
<ContentPage Icon="hamburger_menu.png" Title="Daily Expense" BackgroundColor="#000000"> <!-- Menu Title background color -->
<StackLayout VerticalOptions="FillAndExpand">
<ListView x:Name="MenuListView" ItemsSource="{Binding MainMenuItems}" ItemSelected="MainMenuItem_Selected" VerticalOptions="FillAndExpand" SeparatorVisibility="None" BackgroundColor="#f5f5f5"> <!-- Menu background color -->
<ListView.ItemTemplate>
<DataTemplate>
<ImageCell Text="{Binding Title}" ImageSource="{Binding Icon}" TextColor="Black"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage>
</MasterDetailPage.Master>
</MasterDetailPage>
MenuMasterPage.cs code
public partial class MenuMasterPage : MasterDetailPage
{
public List<MainMenuItem> MainMenuItems { get; set; }
public MenuMasterPage()
{
// Set the binding context to this code behind.
BindingContext = this;
// Build the Menu
MainMenuItems = new List<MainMenuItem>()
{
new MainMenuItem() { Title = "Add Daily Expense", Icon = "menu_inbox.png", TargetType = typeof(Page1) },
new MainMenuItem() { Title = "My Expenses", Icon = "menu_stock.png", TargetType = typeof(Page2) }
};
// Set the default page, this is the "home" page.
Detail = new NavigationPage(new Page1());
InitializeComponent();
}
// When a MenuItem is selected.
public void MainMenuItem_Selected(object sender, SelectedItemChangedEventArgs e)
{
var item = e.SelectedItem as MainMenuItem;
if (item != null)
{
if (item.Title.Equals("Add Daily"))
{
Detail = new NavigationPage(new AddDailyExpensePage());
}
else if (item.Title.Equals("My Expenses"))
{
Detail = new NavigationPage(new MyExpensesPage());
}
MenuListView.SelectedItem = null;
IsPresented = false;
}
}
}
public class MainMenuItem
{
public string Title { get; set; }
public Type TargetType { get; set; }
public string Icon { get; set; }
}
In your App.xaml.cs replace the code with this
public App()
{
InitializeComponent();
{
MainPage = new MenuMasterPage();
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
}
Can you use the Forms MasterDetail page? https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/navigation/master-detail-page/