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

Posted

技术标签:

【中文标题】在 Xamarin.Forms 中使用 MVVM 进行页面导航【英文标题】:Page Navigation using MVVM in Xamarin.Forms 【发布时间】:2017-09-01 10:30:27 【问题描述】:

我正在开发 xamarin.form 跨平台应用程序,我想通过单击按钮从一个页面导航到另一个页面。因为我不能在 ViewModel 中做Navigation.PushAsync(new Page2());,因为它只能在 Code-Behid 文件中。请提出任何方法来做到这一点?

这是我的观点:

<?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>

这是我的 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));
    

【问题讨论】:

你可以试试freshmvvm,很容易理解像魅力一样的作品 【参考方案1】:

一种方法是您可以通过 VM 构造函数传递导航。由于页面继承自VisualElement,因此它们直接继承Navigation 属性。

文件隐藏代码:

public class SignIn : ContentPage

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

然后在您的 VM 中,添加 INavigation 属性并更改构造函数以接受 INavigation。然后,您可以使用此属性进行导航:

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());
    
    ...

请注意您应该修复的代码问题:GoToPage2() 方法必须设置为 async 并返回 Task 类型。此外,该命令将执行异步操作调用。这是因为您必须异步进行页面导航!

希望对你有帮助!

【讨论】:

它可以工作,但是如果我们想在 Xaml 视图文件中绑定上下文怎么办。我们如何将参数传递给视图模型的构造函数?在 Code Behid File 或 Xaml 中绑定上下文的最佳方法是什么? @WaleedArshad:DataContextInitializeComponent() 调用之后创建。所以你可以在之后将它传递给 ViewModel。我通常在构造函数中设置 ViewModel 以防它需要额外的参数(这是我尝试放入后面代码的唯一代码)。我也发现它更容易阅读。如果你没有参数,它是一个选择问题。 我很抱歉加入进来 - 但这绝对违反了所有 MVVM / MVC / MVWhatever 规则......你的 ViewModel 永远不应该创建视图 - 它是反过来的。所有这些概念的目标是将视图与模型分开,因此视图是可交换的,在您的示例中并非如此。想象一下为 View、ViewModel 和 Model 做单独的项目(真正的 *.csproj 文件),你就会看到问题所在。来自 WPF 背景,我来这里看看 Xamarin 人是如何做到这一点的,直到现在我对我发现的概念并不满意......会继续寻找! @JessicaMiceli 我明白你的意思,但这就是 Xamarin 的设计工作方式。然后你仍然可以实现自己的依赖注入服务或使用免费的框架为你做这件事【参考方案2】:

一个简单的方法是

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

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

【讨论】:

这也有效。这是导航到其他页面的非常简单的方法。 为什么所有网站、文档和博客都不使用这个非常简单的解决方案? 我的应用以一个不包含导航的选项卡式页面开始。所以我不能在视图模型中使用“MainPage.Navigation”。如何访问包含导航的子页面? 问题出在哪里【参考方案3】:

从你的虚拟机

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

            
        

【讨论】:

【参考方案4】:

我对此进行了研究,这实际上取决于您要如何处理导航。你想要你的视图模型来处理你的导航还是你想要你的视图。我发现让我的视图处理我的导航是最容易的,这样我就可以为不同的情况或应用程序选择不同的导航格式。在这种情况下,不使用命令绑定模型,而是使用按钮点击事件,并在后面的代码中将新页面添加到导航堆栈中。

将您的按钮更改为:

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

在您的代码中,实现该方法并在那里进行导航。

public void Button_Clicked(object sender, EventArgs e)

    Navigation.PushAsync(new Page2());

如果您正在寻找基于视图模型的导航,我相信有一种方法可以使用 MvvmCross,但我不熟悉该工具。

【讨论】:

【参考方案5】:

我的方法基于原则,每个视图只能导航到应用程序的基于 VM 上下文的位置:

在 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);
    

并将代码隐藏类分配为 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));
    

【讨论】:

【参考方案6】:

通过 VM 构造函数传递 INavigation 确实是一个很好的解决方案,但如果您有深度嵌套的 VM 架构,它的代码也可能非常昂贵。

使用可从任何视图模型访问的单例包装 INavigation 是一种替代方法:

NavigationDispatcher 单例:

 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;
        
    

在 App.xaml.cs 中初始化:

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

在任何 ViewModel 中使用:

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

【讨论】:

【参考方案7】:

决定添加两种将 Page 实例传递给 viewmodel 的方法,您可以稍后将其用于导航、显示警报。关闭页面等等。

1.如果您可以使用命令参数传递它

在视图模型中:

public ICommand cmdAddRecord  get; set; 

视图模型构造函数

cmdAddRecord = new Command<ContentPage>(AddRecord);

视图模型中的某处

    void AddRecord(ContentPage parent)
    
        parent.Navigation.Whatever
    

XAML

标题

            x:Name="thisPage"

用法

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

2。开始在我的视图模型基类中使用它

视图模型

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

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

我们来了

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

子视图模型

public class cPetEventsListVm : cMyBaseVm

现在,围绕子视图模型,我们可以使用 Page ,如 Parent.DisplayAlert 或 Parent.Navigation.PushAsync 等 我们现在甚至可以使用 Parent.PopAsync () 从视图模型关闭页面;

【讨论】:

【参考方案8】:

当我切换到 Xamarin 开发时,我绞尽脑汁几天遇到了同样的障碍。

所以我的答案是将页面的类型放在模型中,但不限制视图或视图模型也可以使用它,如果有人选择的话。这使系统保持灵活,因为它不会通过视图或代码隐藏中的硬接线来绑定导航,因此它更便携。您可以在项目中重复使用您的模型,并且只需设置当这种情况到达其他项目时它将导航到的 Page 的类型。

为此,我生成了一个 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();
        
    

还有一个 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);
        
    

现在模型可能有一个页面的可分配类型,因此它可以更改,并且页面可以在您的设备类型(例如电话、手表、androidios)之间有所不同。示例:

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

还有一个在 Xaml 中使用的示例。

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

为了完整起见,App.xaml 中定义的资源

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

附:虽然命令模式通常应该为一个操作使用一个实例,但在这种情况下,我知道在所有控件中重用同一个实例是非常安全的,而且由于它是用于可穿戴设备,我想让事情比平常更轻,因此定义一个实例App.xaml 中的 NavigationCommand。

【讨论】:

以上是关于在 Xamarin.Forms 中使用 MVVM 进行页面导航的主要内容,如果未能解决你的问题,请参考以下文章

Xamarin.Forms 条目 - 自定义行为和 MVVM

Xamarin.Forms:如何避免在 MVVM 绑定中硬编码字符串

Xamarin.forms MVVM。列表视图仍然为空

在 Xamarin.Forms 项目中实现 MVVM

从按钮命令 xamarin.forms MVVM 获取 ListView

Xamarin Forms MvvM框架之FreshMvvM翻译一