如何在不使用任何框架(如 PRISM 或任何导航服务)的情况下按照 Xamarin.Forms 中的 MVVM 架构进行导航?

Posted

技术标签:

【中文标题】如何在不使用任何框架(如 PRISM 或任何导航服务)的情况下按照 Xamarin.Forms 中的 MVVM 架构进行导航?【英文标题】:How to navigate as per MVVM architecture in Xamarin.Forms without using any frameworks like PRISM or any Navigation Service? 【发布时间】:2020-03-30 05:35:20 【问题描述】:

我有一个继承 PageBase.xamlLoginPage.xamlLoginViewModelLoginPageViewModel,它继承了 BaseViewModelBaseViewModel 定义了 Func 委托 - OnModalNavigationRequestPageBase 在其 OnAppearing 中订阅HandleModalNavigationRequest 方法中提供了方法和实现。当LoginPage.xaml上的Login Button被点击时,LoginViewModelOnSubmit方法,通过命令绑定被调用,验证成功后,调用 BaseViewModelNavigateToModal 方法。 BaseViewModelNavigateToModel 方法调用 Func 委托 PageBaseHandleModalNavigationRequest 方法OnModalNavigationRequest 在检查它是否不是 null 之后。

我的问题是 OnModalNavigationRequestalwaysnull 的形式出现,表明它没有订阅者。这意味着 PageBaseOnAppearing 方法没有被调用,这意味着 PageBase 没有被实例化。如何解决这个问题?

我正在发布正在发生的事件序列的代码:

免责声明:代码帮助来自here

App.xaml.cs

    protected override void OnStart()
    
        // Handle when your app starts
        var properties = Current.Properties;

        if (properties.ContainsKey("Username"))
        
            if (properties["Username"] != null)
            
                var loggedInUser = (string)properties["Username"];
                MainPage = new MainPage();
            
            else
            
                MainPage = new LoginPage();
            
        
        else
        
            MainPage = new LoginPage();
        
    

IView

public interface IView



public interface IView<TViewModel> : IView where TViewModel : BaseViewModel

    TViewModel ViewModel  get; set; 

BaseViewModel

    public class BaseViewModel : INotifyPropertyChanged
    
       private string title;
       public string Title
       
         get  return GetProperty<string>("title"); 
         set  SetProperty(value, "title"); 
       

       protected bool SetProperty<T>(T value,
        [CallerMemberName] string propertyName = "",
        Action onChanged = null)
       
         if (!properties.ContainsKey(propertyName))
         
            properties.Add(propertyName, default(T));
         

         var oldValue = GetProperty<T>(propertyName);
         if (EqualityComparer<T>.Default.Equals(oldValue, value))
            return false;

         properties[propertyName] = value;
         onChanged?.Invoke();
         OnPropertyChanged(propertyName);
         return true;
       

       protected T GetProperty<T>([CallerMemberName] string propertyName = null)
       
         if (!properties.ContainsKey(propertyName))
         
            return default(T);
         
         else
         
            return (T)properties[propertyName];
         
       

       public Func<BaseViewModel, Task> OnModalNavigationRequest  get; set; 

       public async Task NavigateToModal<TViewModel>(TViewModel targetViewModel) where TViewModel : BaseViewModel
      
        await OnModalNavigationRequest?.Invoke(targetViewModel);
      
    

ViewResolver

    internal static class ViewResolver
    
        public static Page GetViewFor<TargetViewModel>(TargetViewModel targetViewModel) where 
TargetViewModel : BaseViewModel, new()
        
            var targetViewName = targetViewModel.GetType().Name.Replace("ViewModel", "Page");
            var definedTypes = targetViewModel.GetType().GetTypeInfo().Assembly.DefinedTypes;
            var targetType = definedTypes.FirstOrDefault(t => t.Name == targetViewName);
            return Activator.CreateInstance(targetType.AsType()) as Page;
        
    

PageBase.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"
         xmlns:d="http://xamarin.com/schemas/2014/forms/design"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
         mc:Ignorable="d"
         x:Class="IPMS.Views.Shared.PageBase">
    </ContentPage>

PageBase.xaml.cs

    public partial class PageBase<TViewModel> : ContentPage, IView<TViewModel> where TViewModel : BaseViewModel, new()
   
    public TViewModel ViewModel
    
        get
        
            return GetValue(BindingContextProperty) as TViewModel;
        
        set
        
            SetValue(BindingContextProperty, value);
        
    

    #region <Events>

    protected override void OnAppearing()
    
        base.OnAppearing();

        ViewModel.OnModalNavigationRequest = HandleModalNavigationRequest;
    

    protected override void OnDisappearing()
    
        base.OnDisappearing();

        ViewModel.OnModalNavigationRequest = null;
    
    #endregion

    async Task HandleModalNavigationRequest(BaseViewModel targetViewModel)
    
        var targetView = ViewResolver.GetViewFor(targetViewModel);
        targetView.BindingContext = targetViewModel;
        await Navigation.PushModalAsync(new NavigationPage(targetView));
    

LoginPage.xaml

    <?xml version="1.0" encoding="utf-8" ?>
    <views:PageBase xmlns="http://xamarin.com/schemas/2014/forms"
         xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
         xmlns:views="clr-namespace:IPMS.Views.Shared;assembly=IPMS"
         x:Class="IPMS.Views.Shared.LoginPage"
         Title="Binding Title">

         <ContentPage.Resources>
           <ResourceDictionary>
             // Resources goes here
           </ResourceDictionary>
         </ContentPage.Resources>

         <ContentPage.Content>
           <StackLayout>
                <Button Command="Binding SubmitCommand" Text="Login" TextColor="White"  
                FontAttributes="Bold" FontSize="Large" HorizontalOptions="FillAndExpand" />
            </StackLayout>
         </ContentPage.Content>
    </views:PageBase>

LoginPage.xaml.cs

    public partial class LoginPage : PageBase
    
        LoginViewModel ViewModel => BindingContext as LoginViewModel;

        public LoginPage() : base()
        
            BindingContext = new LoginViewModel();

            InitializeComponent();
        
    

LoginViewModel.cs

        public class LoginViewModel : BaseViewModel
        
            public Command SubmitCommand  protected set; get; 

            public LoginViewModel() : base()
            
                Title = "Login";
                SubmitCommand = new Command(async () => await OnSubmit());
            

            public async Task OnSubmit()
            
                await NavigateToModal<ItemsViewModel>(new ItemsViewModel());
            
        

【问题讨论】:

,根据文章,我发现作者提供的很简单,所以你尝试作者的样本吗? 是的,但示例不包括从 PageBase.xaml 继承的页面,这是我在某些链接中面临挑战的地方,因为我们无法继承 XAML因为我刚开始使用 Xamarin,所以我不知道 x:TypeArguments 的作用。 【参考方案1】:

得到它的工作

    LoginPage.xaml中添加x:TypeArguments如下所示:

    <views:PageBase xmlns="http://xamarin.com/schemas/2014/forms"
         xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
         xmlns:views="clr-namespace:IPMS.Views.Shared;assembly=IPMS"
         xmlns:ViewModel="clr-namespace:IPMS.ViewModels.Shared;assembly=IPMS"
         x:Class="IPMS.Views.Shared.LoginPage"
         x:TypeArguments="ViewModel:LoginViewModel"
         BackgroundImageSource="loginbg.9.jpg"
         Title="Binding Title">
    </views:PageBase>
    

    修改LoginPage.xaml.cs如下图:

    public partial class LoginPage : PageBase<LoginViewModel>
    
        public LoginPage() : base()
        
            BindingContext = new LoginViewModel();
    
            InitializeComponent();
        
    
    

【讨论】:

很高兴听到您自己解决了您的问题,所以请记得将您的回复标记为答案,谢谢。

以上是关于如何在不使用任何框架(如 PRISM 或任何导航服务)的情况下按照 Xamarin.Forms 中的 MVVM 架构进行导航?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Prism 的“OnNavigatingTo”中为任何和所有类执行操作

.NET Core 3 WPF MVVM框架 Prism系列之导航系统

在 PRISM 4 中导航到新视图时如何改进传递对象

.NET MAUI实战 Routing

Prism:如何在区域中注入视图模型实例?

在 Wpf 应用程序中使用 Prism 进行导航