65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 MVVM 为 Xamarin Forms 创建带标签页的界面, 使用 Calcium

starIconstarIconstarIconstarIconstarIcon

5.00/5 (10投票s)

2014年9月17日

CPOL

8分钟阅读

viewsIcon

55364

通过绑定到ViewModel集合来创建Xamarin Forms标签页或轮播页;扩展Xamarin Forms的现有功能。

Calcium for Xamarin Forms

引言

Xamarin.Forms 是 Xamarin 的新跨平台 UI 框架,允许您使用 XAML 为 iOS、Android 和 Windows Phone 构建用户界面。

在本系列文章的上一部分,我们回顾了Calcium的起源并简要介绍了它的一些关键功能。您已经了解了如何安装Calcium NuGet包。您还学习了如何创建Xamarin Forms共享项目,以及如何使用Bootstrapper初始化您的应用程序。您看到了如何在XAML中创建基本页面以及如何将该页面绑定到ViewModel。最后,您了解了Calcium的一些默认服务,包括Dialog Service、Settings Service和User Options Service。

在本系列的第二部分,您将学习如何扩展Xamarin Forms的现有API功能,通过绑定到ViewModel集合来创建标签页或轮播页。您将学习如何实现一个准数据模板选择器,以使用MVVM来实例化ViewModels。

如果您还没有阅读,我建议您在阅读本文之前,先阅读第一篇文章

本系列文章

Calcium 和示例应用程序的源代码

Calcium for Xamarin.Forms 及示例的源代码位于 https://calcium.codeplex.com/SourceControl/latest

存储库中有各种解决方案。您感兴趣的是位于 \Trunk\Source\Calcium\Xamarin\Installation 目录下的 CalciumTemplates.Xamarin.sln。

CalciumTemplates.Xamarin 解决方案的结构如图1所示。您可以注意到示例“模板”项目已被高亮显示。这些项目包含了本系列文章中介绍的大部分示例源代码。

Solution Structure

图1. CalciumTemplates.Xamarin 解决方案结构

在Xamarin.Forms中创建标签页

TabbedPage 提供了一种用户友好的方式来展示内容,这些内容分布在多个页面上,但都显示在同一个视图中。让我们看看在XAML中创建标签页的传统方法。您可以通过创建一个新的Forms XAML Page并将根节点类型更改为TabbedPage来实现,如下面的代码片段所示。

<?xml version="1.0" encoding="utf-8" ?>
<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms"                                     
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    x:Class="CalciumTemplateApp.Views.HubView">

       <TabbedPage.Children>
              <ContentPage />
              <ContentPage />
              <ContentPage />
       </TabbedPage.Children>

</TabbedPage>

子页面嵌套在TabbedPage元素的Children元素内。

这种方法的缺点是它不够灵活,因为它要求您预先知道将要放入TabbedPage的内容。

TabbedPage还提供了一个ItemTemplate属性,允许您以统一的方式实例化一组对象,如下所示。

<TabbedPage.ItemTemplate>
       <DataTemplate>
              <ContentPage Title="{Binding Title}" />
       </DataTemplate>
</TabbedPage.ItemTemplate>

这种方法在您有一组同类型对象并希望以相同方式渲染它们时很有用。缺点是它不允许您改变对象的渲染方式;您只有一个数据模板,就这样了。

我遇到过的一个常见需求是,您拥有一组不同类型的对象,需要以不同的方式呈现。不幸的是,Xamarin Forms还没有像WPF那样的数据模板选择器。然而,在本节中,我将演示如何创建一个准模板选择器,它允许您将ObservableList的ViewModels绑定到一个TabbedPage;这让我们离MVVM的极乐世界又近了一步。

现在让我们看一个具体的例子。

HubViewModel包含一个ViewModels的ObservableCollection。当创建HubViewModel时,该集合会填充AboutViewModel和OptionsViewModel,如下面的代码片段所示。

public HubViewModel() : base(AppResources.Views_Hub_Title)
{
    var optionsViewModel = Dependency.Resolve<OptionsViewModel>();
    ViewModels.Add(optionsViewModel);
    
    var aboutViewModel = Dependency.Resolve<AboutViewModel>();
    ViewModels.Add(aboutViewModel);
}

 

HubView是一个TabbedPage,其ItemsSource属性绑定到ViewModel的子ViewModels集合,如下面的代码片段所示。

<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
            xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
            x:Class="CalciumSampleApp.Views.HubView"
            xmlns:calcium="clr-namespace:Outcoder.UI.Xaml;assembly=Outcoder.Calcium"

            ItemsSource="{Binding ViewModels}">

       <TabbedPage.ItemTemplate>
              <DataTemplate>
                     <calcium:ViewLocatorPage Title="{Binding Title}" />
              </DataTemplate>
       </TabbedPage.ItemTemplate>

</TabbedPage>

在前一个代码片段中的ViewLocatorPage派生自ContentPage,其任务是根据其BindingContext属性的值来填充自身。当它检测到其BindingContext属性发生变化时,它会通过订阅自身的BindingContextChanged事件来实现。请参阅列表1。

您会注意到,如果父对象的BindingContext与页面本身的BindingContext相同,则会忽略更改事件。这是因为直到页面的binding context显式地分配了来自集合的对象,它才继承其父对象的BindingContext。这是binding context继承,并且由TabbedPage的基类BindableObject的SetInheritedBindingContext方法为您完成。但我离题了。

回到HandleBindingContextChanged方法,通常的绑定视图到ViewModel的步骤继续进行,ViewSelectorPage的Content属性被切换到解析出的视图。这本质上是数据模板选择器步骤。

注意. TabbedPage中显示的每个视图都必须派生自View类。

我在ViewLocatorPage中添加了一个扩展点,即ViewLocator属性。ViewLocator中包含实际的查找视图的逻辑。

列表1. ViewLocatorPage.HandleBindingContextChanged 方法

void HandleBindingContextChanged(object sender, EventArgs e)
{
       var context = BindingContext;                    

       if (context == null)
       {
              Content = null;
              return;
       }                    

       var parent = Parent;

       if (parent != null && parent.BindingContext == context)
       {
              /* The BindingContext may be set to the parent's context
               * before being set to a child viewmodel. */
              return;
       }

       IViewLocator locator = ViewLocator;

       if (locator == null)
       {
              ViewLocator = locator = this;
       }

       var newContent = locator.LocateView(BindingContext, Content);

       if (newContent != Content)
       {
              Content = newContent;
       }
}

IViewLocator接口包含一个名为LocateView的单一方法,该方法接受binding context对象(ViewModel)和当前用于呈现ViewModel的视图。LocateView方法返回一个Xamarin.Forms.View实例。

IViewLocator的默认实现是ViewLocatorPage本身。它显式地实现了该接口,如列表2所示。视图根据命名约定进行查找,即ViewModel以Model为后缀,其对应的视图存在于同一个程序集和同一个命名空间中。当能够解析视图类型时,将使用依赖项系统构建对象。然后将视图返回给ViewLocatorPage,后者将其分配给其Content属性。

列表2. ViewLocatorPage LocateView 方法

View IViewLocator.LocateView(object bindingContext, View currentView)
{
       if (bindingContext == null)
       {
              return null;
       }


       Type bindingContextType = bindingContext.GetType();
       string typeName = bindingContextType.FullName;

       const string viewModelSuffix = "Model";

       if (typeName.EndsWith(viewModelSuffix))
       {
              string viewName = typeName.Substring(0, typeName.Length - viewModelSuffix.Length);

              var currentContent = currentView;

              if (currentContent != null && currentContent.GetType().FullName == viewName)
              {
                     return currentView;
              }

              var assembly = bindingContextType.Assembly;

              Type type = Type.GetType(viewName + ", " + assembly.FullName, false);

              if (type != null)
              {
                     var content = Dependency.Resolve(type);
                     View view = (View)content;
                     view.BindingContext = bindingContext;
                     view.BindToInfrastructure();

                     return view;
              }
       }

       return null;
}

您可以通过将不同的IViewLocator对象分配给ViewLocatorPage的ViewLocator属性来更改视图的查找方式。请随意获取代码并进行修改,以支持您所需的任何自定义场景。如果您认为某些内容对他人有益,请告诉我,我会将其合并到代码库中。

集成可绑定属性

Xamarin Forms包含了WPF、Silverlight或WinRT的DependencyProperty的等价物,但在Xamarin中它们被称为BindableProperty。它们在大多数方面都是等价的。它们通常被定义为BindableObject的静态成员,在本例中是ViewLocatorPage。

注意. 虽然DependencyProperty和BindableProperty类型有许多相似之处,但也存在一些显著的差异。其中之一是在Xamarin Forms中,无法从BindableObject检索特定的Binding对象来进行重新绑定或检查绑定路径。我发现这在WPF、Silverlight和WinRT中非常有用。

ViewLocator定义如下:

public static readonly BindableProperty ViewLocatorProperty;

ViewLocatorPage的静态构造函数按照如下方式初始化BindableProperty:

static ViewLocatorPage()
{
       ViewLocatorProperty = BindableProperty.Create("ViewLocator",
              typeof(IViewLocator), typeof(ViewLocatorPage),
              null, BindingMode.TwoWay);
}

一个实例属性将BindableProperty暴露给消费者。GetValue和SetValue方法对于有经验的XAML开发人员来说应该很熟悉。这两个方法都继承自BindableObject基类。以下代码片段显示了ViewLocatorPage类的ViewLocator属性。

public IViewLocator ViewLocator
{
       get
       {
              return (IViewLocator)GetValue(ViewLocatorProperty);
       }
       set
       {
              SetValue(ViewLocatorProperty, value);
       }
}

这使得ViewLocator可以与任何其他对象进行数据绑定。

注意. 数据绑定的限制与WPF、Silverlight和WinRT中的情况相同。在Xamarin Forms中,数据绑定的目标(例如ViewModel)可以是任何对象,但源必须是BindableObject。请记住,源可以是任何对象,但目标必须是BindableObject。

ViewSelectorPage是一个ContentPage,因此其Content属性必须解析为Xamarin.Forms.View对象。因此,呈现每个ViewModel的视图必须派生自Xamarin.Forms.View。这比Windows Phone Silverlight的功能还要受限一些,但可以接受。不过,如果您计划在MultiPage<T>之外实例化ContentViews,您需要进行一些重构。

图2显示了TabbedPage如何在三个平台(Windows Phone、Android和iOS)上呈现。

Tabbed page displayed for all three platforms.

图2. TabbedPage在三个平台上有特定的表示。

应用准数据模板使您能够构造由子ViewModel集合组成的复合ViewModel,并允许您从复合ViewModel中添加新视图、协调视图之间的活动以及在业务逻辑中动态添加和删除视图。

防止方向更改重启您的Android Activity

在结束本文之前,我想简要地提请您注意一下在Android应用中构建Xamarin Forms时很重要的一点。默认情况下,当用户更改设备的屏幕方向时,Android应用的当前Activity会重置。这可能会对您的视图造成破坏,并将您的布局重置为主视图。要防止这种情况发生,请在主Activity的Activity属性中包含一个ConfigurationChanges属性,如下所示。

[Activity(Label = "Calcium Example", MainLauncher = true,
              ConfigurationChanges = Android.Content.PM.ConfigChanges.Orientation
             | Android.Content.PM.ConfigChanges.ScreenSize)]
public class MainActivity : AndroidActivity
{
...
}

结论

在本文中,您看到了如何通过绑定到ViewModel集合来创建标签页或轮播页。您学习了如何实现一个准数据模板选择器,以使用MVVM来实例化ViewModels。

在下一篇文章,第三部分. 使用Xamarin Forms和Calcium构建本地化的跨平台应用程序中,您将学习如何应用相同的原理来显示用户选项的分类列表。选项可以通过一行代码添加。模板控制选项类型的显示方式。选项系统与设置服务协同工作,自动将更改写回持久存储。这将很有趣,所以我希望您能加入我。

我希望您觉得这个项目有用。如果有用,我将不胜感激您能对其进行评分和/或在下方留下反馈。这将帮助我写出更好的下一篇文章。

历史

2014年9月

  • 首次发布。
© . All rights reserved.