简体   繁体   English

在 Xamarin.Forms 中使用 MVVM 进行页面导航

[英]Page Navigation using MVVM in Xamarin.Forms

I am working on xamarin.form cross-platform application , i want to navigate from one page to another on button click.我正在开发 xamarin.form 跨平台应用程序,我想通过单击按钮从一个页面导航到另一个页面。 As i cannot do Navigation.PushAsync(new Page2());因为我不能做Navigation.PushAsync(new Page2()); in ViewModel because it only possible in Code-Behid file.在 ViewModel 中,因为它只能在 Code-Behid 文件中使用。 please suggest any way to do this?请建议任何方法来做到这一点?

Here is my View:这是我的观点:

<?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="Calculator.Views.SignIn"
             xmlns:ViewModels="clr-namespace:Calculator.ViewModels;assembly=Calculator">
    
    <ContentPage.BindingContext>
        <ViewModels:LocalAccountViewModel/>
    </ContentPage.BindingContext>
    
    <ContentPage.Content>    
        <StackLayout>
            <Button Command="{Binding ContinueBtnClicked}" />    
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

Here is my ViewModel:这是我的视图模型:

public class LocalAccountViewModel : INotifyPropertyChanged
{
    public LocalAccountViewModel()
    {
        this.ContinueBtnClicked = new Command(GotoPage2);
    }
        
    public void GotoPage2()
    {
        /////
    }
    
    public ICommand ContinueBtnClicked
    {
        protected set;
        get;
    }
    
    public event PropertyChangedEventHandler PropertyChanged;
    
    protected virtual void OnPropertyChanges([CallerMemberName] string PropertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropertyName));
    }
}

One way is you can pass the Navigation through the VM Constructor.一种方法是您可以通过 VM 构造函数传递导航。 Since pages inherit from VisualElement , they directly inherit the Navigation property.由于页面从VisualElement继承,因此它们直接继承Navigation属性。

Code behind file:文件隐藏代码:

public class SignIn : ContentPage
{
    public SignIn(){
       InitializeComponent();
       // Note the VM constructor takes now a INavigation parameter
       BindingContext = new LocalAccountViewModel(Navigation);
    }
}

Then in your VM, add a INavigation property and change the constructor to accept a INavigation .然后在您的 VM 中,添加INavigation属性并将构造函数更改为接受INavigation You can then use this property for navigation:然后,您可以使用此属性进行导航:

public class LocalAccountViewModel : INotifyPropertyChanged
{
    public INavigation Navigation { get; set;}

    public LocalAccountViewModel(INavigation navigation)
    {
        this.Navigation = navigation;
        this.ContinueBtnClicked = new Command(async () => await GotoPage2());
    }

    public async Task GotoPage2()
    {
        /////
        await Navigation.PushAsync(new Page2());
    }
    ...
}

Note an issue with your code that you should fix: The GoToPage2() method must be set async and return the Task type.请注意您应该修复的代码问题:必须将GoToPage2()方法设置为async并返回Task类型。 In addition, the command will perform an asynchronous action call.此外,该命令将执行异步操作调用。 This is because you must do page navigation asychronously!这是因为您必须异步进行页面导航!

Hope it helps!希望能帮助到你!

A simple way is一个简单的方法是

this.ContinueBtnClicked = new Command(async()=>{

    await Application.Current.MainPage.Navigation.PushAsync(new Page2());
});

From your VM从你的虚拟机

public Command RegisterCommand
        {
            get
            {
                return new Command(async () =>
                {
                    await Application.Current.MainPage.Navigation.PushAsync(new RegisterNewUser());
                });

            }
        }

I looked into this, and it really depends on how you want to handle your navigation.我研究了这个,这真的取决于你想如何处理你的导航。 Do you want your view models to handle your navigation or do you want your views.你想让你的视图模型来处理你的导航还是你想要你的视图。 I found it easiest to have my views handle my navigation so that I could choose to have a different navigation format for different situations or applications.我发现让我的视图处理我的导航最容易,这样我就可以为不同的情况或应用程序选择不同的导航格式。 In this situation, rather than using the command binding model, just use a button clicked event and add the new page to the navigation stack in the code behind.在这种情况下,而不是使用命令绑定模型,只需使用按钮单击事件并将新页面添加到后面代码中的导航堆栈中。

Change your button to something like:将您的按钮更改为:

<StackLayout>
    <Button Clicked="Button_Clicked"></Button>
</StackLayout>

And in your code behind, implement the method and do the navigation there.在后面的代码中,实现该方法并在那里进行导航。

public void Button_Clicked(object sender, EventArgs e)
{
    Navigation.PushAsync(new Page2());
}

If you are looking to do viewmodel based navigation, I believe there is a way to do this with MvvmCross, but I am not familiar with that tool.如果您正在寻找基于视图模型的导航,我相信有一种方法可以使用 MvvmCross 来做到这一点,但我不熟悉该工具。

my approach based on principle every View can navigate to VM context based places of the app only:我的方法基于原则,每个视图只能导航到基于 VM 上下文的应用程序位置:

In ViewModel i declare INavigationHandler interfeces like that:在 ViewModel 中,我声明 INavigationHandler 接口如下:

public class ItemsViewModel : ViewModelBase
{
    public INavigationHandler NavigationHandler { private get; set; }


    // some VM code here where in some place i'm invoking
    RelayCommand<int> ItemSelectedCommand => 
        new RelayCommand<int>((itemID) => { NavigationHandler.NavigateToItemDetail(itemID); });


    public interface INavigationHandler
    {
        void NavigateToItemDetail(int itemID);
    }
}

And assign code-behind class as INavigationHandler for ViewModel:并将代码隐藏类分配为 ViewModel 的 INavigationHandler:

public class ItemsPage : ContentPage, ItemsViewModel.INavigationHandler
{
    ItemsViewModel viewModel;

    public ItemsPage()
    {
        viewModel = Container.Default.Get<ItemsViewModel>();
        viewModel.NavigationHandler = this;
    }


    public async void NavigateToItemDetail(int itemID)
    {
        await Navigation.PushAsync(new ItemDetailPage(itemID));
    }
}

Passing INavigation through VM constructor is a good solution indeed, but it can also be quite code-expensive if you have deep nested VMs architecture.通过 VM 构造函数传递 INavigation 确实是一个很好的解决方案,但如果您有深层嵌套的 VM 架构,它也可能非常昂贵。

Wrapping INavigation with a singleton, accesible from any view model, is an alternative:使用可从任何视图模型访问的单例包装 INavigation 是一种替代方法:

NavigationDispatcher Singleton: NavigationDispatcher Singleton:

 public class NavigationDispatcher
    {
        private static NavigationDispatcher _instance;

        private INavigation _navigation;

        public static NavigationDispatcher Instance =>
                      _instance ?? (_instance = new NavigationDispatcher());

        public INavigation Navigation => 
                     _navigation ?? throw new Exception("NavigationDispatcher is not initialized");

        public void Initialize(INavigation navigation)
        {
            _navigation = navigation;
        }
    }

Initializing in App.xaml.cs:在 App.xaml.cs 中初始化:

       public App()
       {
          InitializeComponent();
          MainPage = new NavigationPage(new MainPage());
          NavigationDispatcher.Instance.Initialize(MainPage.Navigation);
       }

Using in any ViewModel:在任何 ViewModel 中使用:

 ...
 private async void OnSomeCommand(object obj)
        {
            var page = new OtherPage();
            await NavigationDispatcher.Instance.Navigation.PushAsync(page);
        }
 ...

decided to add two ways to pass Page instance to viewmodel which you can use later for navigation, displaying alerts.决定添加两种方法将 Page 实例传递给 viewmodel,您可以稍后使用它进行导航,显示警报。 closing page and so on.关闭页面等。

1. if you can pass it with command parameter 1.如果你可以通过命令参数传递它

in view model:在视图模型中:

public ICommand cmdAddRecord { get; set; }

viewmodel constructor视图模型构造函数

cmdAddRecord = new Command<ContentPage>(AddRecord);

somewhere in viewmodel视图模型中的某处

    void AddRecord(ContentPage parent)
    {
        parent.Navigation.Whatever
    }

XAML XAML

header标题

            x:Name="thisPage"

usage用法

 <ToolbarItem IconImageSource="{StaticResource icAdd}"  Command="{Binding cmdAddRecord}"  CommandParameter="{Binding ., Source={x:Reference thisPage}}" />

2. started using this in my base class for viewmodels 2. 开始在我的视图模型基类中使用它

viewmodel视图模型

public class cMyBaseVm : BindableObject

... ...

public static BindableProperty ParentProperty = BindableProperty.Create("Parent", typeof(ContentPage), typeof(cMyBaseVm), null, BindingMode.OneWay);

... ...

   public ContentPage Parent
    {
        get => (ContentPage)GetValue(ParentProperty);
        set => SetValue(ParentProperty, value);
    }

XAML XAML

        xmlns:viewModels="clr-namespace:yournamespace.ViewModels"
        x:Name="thisPage"

and here we go现在我们开始

<ContentPage.BindingContext>
    <viewModels:cPetEventsListVm Parent="{Binding ., Source={x:Reference thisPage}}" />
</ContentPage.BindingContext>

child viewmodel子视图模型

public class cPetEventsListVm : cMyBaseVm

and now, all around child view model we can use Page like Parent.DisplayAlert , or Parent.Navigation.PushAsync etc we may even close page now from view model with Parent.PopAsync ();现在,在子视图模型周围,我们可以使用 Page 像 Parent.DisplayAlert 或 Parent.Navigation.PushAsync 等,我们现在甚至可以使用 Parent.PopAsync () 从视图模型关闭页面;

I racked my brains on this a few days hitting the same hurdle when I switched to Xamarin development.几天后,当我切换到 Xamarin 开发时,我遇到了同样的障碍,我绞尽脑汁。

So my answer is to put the Type of the page in the Model but not restricting the View or the ViewModel to work with it also if one so chooses.所以我的答案是将页面的类型放在模型中,但不限制视图或视图模型也可以使用它,如果这样选择的话。 This keeps the system flexible in that it does not tie up the Navigation via hard-wiring in the view or in the code-behind and therefore it is far more portable.这使系统保持灵活,因为它不会通过视图或代码隐藏中的硬接线来束缚导航,因此它更便携。 You can reuse your models across the projects and merely set the type of the Page it will navigate to when such circumstance arrive in the other project.您可以在项目中重复使用您的模型,只需设置当这种情况到达另一个项目时它将导航到的页面类型。

To this end I produce an IValueConverter为此我制作了一个 IValueConverter

    public class PageConverter : IValueConverter
    {
        internal static readonly Type PageType = typeof(Page);

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            Page rv = null;

            var type = (Type)value;

            if (PageConverter.PageType.IsAssignableFrom(type))
            {
                var instance = (Page)Activator.CreateInstance(type);
                rv = instance;
            }

            return rv;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var page = (Page)value;
            return page.GetType();
        }
    }

And an ICommand和一个 ICommand

public class NavigateCommand : ICommand
{
    private static Lazy<PageConverter> PageConverterInstance = new Lazy<PageConverter>(true);

    public event EventHandler CanExecuteChanged;

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        var page = PageConverterInstance.Value.Convert(parameter, null, null, null) as Page;

        if(page != null)
        {
            Application.Current.MainPage.Navigation.PushAsync(page);
        }
    }
}

Now the Model may have an assignable Type for a page, therefore it can change, and the page can be different between your device types (eg Phone, Watch, Android, iOS).现在模型可能有一个页面的可分配类型,因此它可以改变,并且页面可以在您的设备类型(例如手机、手表、Android、iOS)之间有所不同。 Example:例子:

        [Bindable(BindableSupport.Yes)]
        public Type HelpPageType
        {
            get
            {
                return _helpPageType;
            }
            set
            {
                SetProperty(ref _helpPageType, value);
            }
        }

And an example of it's use then in Xaml.然后在 Xaml 中使用它的一个例子。

<Button x:Name="helpButton" Text="{Binding HelpButtonText}" Command="{StaticResource ApplicationNavigate}" CommandParameter="{Binding HelpPageType}"></Button>

And for the sake of completeness the resource as defined in App.xaml为了完整起见,App.xaml 中定义的资源

<Application.Resources>
    <ResourceDictionary>   
        <xcmd:NavigateCommand x:Key="ApplicationNavigate" />             
    </ResourceDictionary>
</Application.Resources>

PS While the Command pattern generally should use one instance for one operation, in this case I know it is very safe to reuse the same instance across all controls and since it is for a wearable I want to keep things lighter than normal hence defining a single instance of the NavigationCommand in the App.xaml. PS 虽然命令模式通常应该为一个操作使用一个实例,但在这种情况下,我知道在所有控件中重用同一个实例是非常安全的,而且由于它是用于可穿戴设备,我想让事情比正常情况更轻,因此定义了一个App.xaml 中 NavigationCommand 的实例。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM