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

WinRT: StyleMVVM 演示 2/2

starIconstarIconstarIconstarIconstarIcon

5.00/5 (24投票s)

2013年11月4日

CPOL

14分钟阅读

viewsIcon

47916

StyleMVVM 演示应用。

引言

系列链接

引言

这是我的 StyleMVVM 文章系列的第二部分,也是最后一部分。 上次我们介绍了 StyleMVVM 自带的功能。这次我们将重点介绍我使用 StyleMVVM 创建的一个小型演示应用程序。

在本文及其关联代码(参见文章顶部的代码链接)中,我们将使用上 一篇文章中讨论过的大部分内容。因此,我们可以预期涵盖以下主题:

  • 引导
  • IOC (控制反转)
  • 导航
  • 验证
  • 挂起
  • 核心服务的使用 

前提条件

如果您想尝试代码 (这是我的演示,不是 StyleMVVM 源代码附带的那个),您需要安装以下内容:

  1. Visual Studio 2012 / 2013
  2. Windows 8 / 8.1
  3. SqlLite Windows 8 相关: https://sqlite.ac.cn/2013/sqlite-winrt80-3080002.vsix (虽然已包含在演示代码下载中,您只需运行它。它是一个 Visual Studio 扩展,双击即可)
  4. 完成此处的第 1 步后,请确保所有 NuGet 包都已存在,如果不存在,请还原它们。并且确保您已引用已安装(从第 1 步)的 SQLite 库 

DemoApp 概述

我想为我首次涉足 WinRT 和 Windows 8 开发设计一个相当简单的应用程序,但我也想要一个有足够实质内容,不至于过于简单的东西。我也知道我真的不想编写一个完整的服务器端服务层 (例如 WCF / REST 等)来驱动应用程序,因为我认为这会过多地偏离客户端技术(即 WinRT),而我觉得这正是我希望在这个系列文章中传达的内容。

话虽如此,我知道要做出任何像样的东西,我都需要在某处存储和检索状态。我最初的想法是使用某种 NoSQL 文档存储,如 RavenDB,但遗憾的是,似乎还没有 WinRT 的 RavenDB 客户端,尽管我读过一些相关内容,并且它很快就会推出。

因此,我寻找了一些替代方案并达成了一个折衷方案,它仍然是 SQL 基础的,但又不那么传统,因为它允许通过使用某些属性来生成 SQL,并且还有一个不错的 Visual Studio 扩展来使其与 WinRT 配合使用,并且还具有原生的 async/await 支持。所涉及的数据库是 SQLite,您可以在此处获取扩展: https://sqlite.ac.cn/2013/sqlite-winrt80-3080002.vsix (不过为了方便起见,我已将其包含在本篇文章顶部的可下载应用程序中)。

好的,现在我们知道了涉及的技术

  1. Windows 8
  2. WinRT
  3. SQLLite 

但这个演示应用程序实际上是做什么的?

这个演示应用程序是一个简单的医生诊所患者/预约预订系统。以下是它允许进行的操作的细分:

  1. 捕获基本的患者信息(包括通过网络摄像头捕获的图像)
  2. 允许创建新的预约(演示应用程序中的所有预约都按 1/2 小时时段划分)
  3. 查看当前在诊所工作的医生在给定日期内的所有预约(医生集合是静态的)
  4. 钻取到给定医生的已找到的预约
  5. 查看有关给定日期内针对当前医生安排的预约数量的统计信息。 

DemoApp 详解

在本节中,我们将讨论附加演示应用程序中的各个组件。有一些是共同关注的问题,例如 LayoutAwarePage,我们将首先讨论它,然后详细讨论各个页面。不过,我不会涵盖我在 第一篇文章中已经讨论过的内容,因为那样会显得重复太多了。

LayoutAwarePage

我想要解决的一个横切关注点是让我的应用程序能够显示在所有支持的外形尺寸中。对我来说,这些是 Windows8 模拟器支持的外形尺寸。

因此, namely 模拟器中显示的这些分辨率

我还希望支持任何方向,例如纵向/横向/填充等。我已使用模拟器测试了所有不同的尺寸/方向,它确实是一个有价值的工具。我认为我没有遗漏任何内容,它应该适用于任何尺寸和任何方向。

那么它如何在任何尺寸和任何方向下工作?

使 WinRT 应用程序适用于任何尺寸和方向的技巧是通过 2 个方面实现的:

事物 1:面板

对于尺寸调整,请确保使用标准的面板,例如 GridStackPanel 等。它们内置了非常好的布局算法。所以请使用它们。

事物 2:自适应布局

对于自适应布局,我们使用 StyleMVVM 提供的 LayoutAwarePage。然后我们需要做的是,为各种方向使用一些 VisualStates,例如:

  1. FullScreenLandscape
  2. Filled
  3. FullScreenPortrait
  4. Snapped
<Common:LayoutAwarePage
    ......
    ......
    ......
    ......>
    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">


        <Grid x:Name="notSnappedGrid" 
          Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
            ....
            ....
        </Grid>

        <Grid x:Name="snappedGrid" Visibility="Collapsed">
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" 
            VerticalAlignment="Center">
                <Image Source="../Assets/Warning.png" Width="40" Height="40" 
            Margin="5" VerticalAlignment="Center"/>
                <TextBlock Text="DEMO DOESN'T ALLOW SNAP MODE" 
            Style="{StaticResource SnapModeLabelStyle}"/>
            </StackPanel>
        </Grid>

        <VisualStateManager.VisualStateGroups>

            <!-- Visual states reflect the application's view state -->
            <VisualStateGroup x:Name="ApplicationViewStates">

                <VisualState x:Name="FullScreenLandscape"/>
                <VisualState x:Name="Filled"/>
                <VisualState x:Name="FullScreenPortrait"/>

                <!-- The back button and title have different styles when snapped -->
                <VisualState x:Name="Snapped">
                    <Storyboard>
                        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="notSnappedGrid" 
                Storyboard.TargetProperty="Visibility">
                            <DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed"/>
                        </ObjectAnimationUsingKeyFrames>
                        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="snappedGrid" 
                Storyboard.TargetProperty="Visibility">
                            <DiscreteObjectKeyFrame KeyTime="0" Value="Visible"/>
                        </ObjectAnimationUsingKeyFrames>
                    </Storyboard>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
    </Grid>
</Common:LayoutAwarePage>

诀窍在于,UI 的某些区域将变为可见,而另一些区域将变为不可见,或者根据方向应用不同的 DataTemplate。我认为上述 LayoutAwarePage 涵盖了您需要做的大部分工作。

现在微软有一些关于所有应用都必须支持快照视图的指南。好吧,抱歉,这并不适用于所有应用。例如,我的应用程序在快照模式下并不真正适用,所以如果您尝试快照附加的演示应用程序,您将看到这个,我提供了一个快照视图,基本上说明我的演示应用程序不支持快照模式。

SQL Lite 数据访问服务

该应用程序提供的横切关注点之一是使用 SQLite 数据库,因此,有一个用于与 SQLite 数据库通信的数据访问服务是相当合理的。在演示应用程序中,这作为一个 ISqlLiteDatabaseService 实现提供,其中 ISqlLiteDatabaseService 接口如下所示:

public interface ISqlLiteDatabaseService
{
    IAsyncOperation<int> SavePatientDetailAsync(PatientDetailModel patient);
    IAsyncOperation<int> SaveScheduleItemAsync(ScheduleItemModel scheduleItem);
    IAsyncOperation<List<DoctorModel>> GetDoctorsAsync { get; }
    IAsyncOperation<PatientDetailModel> GetPatientAsync(int patientId);
    IAsyncOperation<List<PatientDetailModel>> GetPatientsAsync();
    IAsyncOperation<List<ScheduleItemModel>> FetchAppointmentsForDoctorAsync(int doctorId, DateTime date);
    IAsyncOperation<bool> DeleteAppointmentAsync(int scheduleItemId);
    IAsyncOperation<Dictionary<DoctorModel, List<ScheduleItemModel>>> FetchScheduleItemsAsync(DateTime date);
    IAsyncOperation<Dictionary<DoctorModel, List<ScheduleItemModel>>> SearchScheduleItemsAsync(string name);
}

而通用实现如下所示(我不想也不需要详细说明每个方法,它们大致遵循相同的思路):

[Singleton]
[Export(typeof(ISqlLiteDatabaseService))]
public class SqlLiteDatabaseService : ISqlLiteDatabaseService
{
    private string dbRootPath = Windows.Storage.ApplicationData.Current.LocalFolder.Path;
    private Lazy<Task<List<DoctorModel>>> doctorsLazy;
    private List<string> doctorNames = new List<string>();

    public SqlLiteDatabaseService()
    {
        doctorNames.Add("Dr John Smith");
        doctorNames.Add("Dr Mathew Marson");
        doctorNames.Add("Dr Fred Bird");
        doctorNames.Add("Dr Nathan Fills");
        doctorNames.Add("Dr Brad Dens");
        doctorNames.Add("Dr Nathan Drews");
        doctorNames.Add("Dr Frank Hill");
        doctorNames.Add("Dr Lelia Spark");
        doctorNames.Add("Dr Amy Wing");
        doctorNames.Add("Dr Bes Butler");
        doctorNames.Add("Dr John Best");
        doctorNames.Add("Dr Philip Mungbean");
        doctorNames.Add("Dr Jude Fink");
        doctorNames.Add("Dr Petra Nicestock");
        doctorNames.Add("Dr Ras Guul");


        doctorsLazy = new Lazy<Task<List<DoctorModel>>>(async delegate 
        {
            List<DoctorModel> doctors = await GetDoctorsInternal();
            return doctors;
        });
    }

    public IAsyncOperation<List<DoctorModel>> GetDoctorsAsync 
    { 
        get
        {
            return doctorsLazy.Value.AsAsyncOperation<List<DoctorModel>>();
        }
    }

    private async Task<List<DoctorModel>> GetDoctorsInternal()
    {
        var db = new SQLiteAsyncConnection(
            Path.Combine(dbRootPath, "StyleMVVM_SurgeryDemo.sqlite"));
        var cta = await db.CreateTableAsync<DoctorModel>();

        var query = db.Table<DoctorModel>();
        var doctors = await query.ToListAsync();
        if (doctors.Count == 0)
        {
            foreach (var doctorName in doctorNames)
            {
                var id = await db.InsertAsync(new DoctorModel() { Name = doctorName });
            }
        }
        query = db.Table<DoctorModel>();
        doctors = await query.ToListAsync();
        return doctors;
    }

    .....
    .....
    .....
    .....
}

因为 SQLite 有一个不错的 .NET 4.5 API,我们可以自由地使用 Async / Await,例如这样:

Doctors = await sqlLiteDatabaseService.GetDoctorsAsync;

主页

主页不过是按钮的集合,每个按钮都允许加载一个页面。我想在这里指出的一点(因为这是我们第一次真正接触 IOC 容器(到目前为止))是如何从容器中导入内容。

这是 MainPageViewModel 的构造函数:

[ImportConstructor]
public MainPageViewModel(IEnumerable<IPageInfo> pageInfos)
{
    Pages = new List<IPageInfo>(pageInfos);
    Pages.Sort((x, y) => x.Index.CompareTo(y.Index));
}

看看我们在这里如何使用 StyleMVVMImportConstructorAttribute 来导入一些页面:

点击图片查看大图

然后我们可以使用类似这样的代码导航到每个页面:

public void ItemClick(ItemClickEventArgs args)
{
    IPageInfo page = args.ClickedItem as IPageInfo;

    if (page != null)
    {
        Navigation.Navigate(page.ViewName);
    }
}

我们在 XAML 中这样触发它:

<Grid x:Name="notSnappedGrid" Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
    <GridView ItemsSource="{Binding Pages}"
            Height="210"
            HorizontalAlignment="Center" VerticalAlignment="Center"
            SelectionMode="None" IsItemClickEnabled="True" 
            View:EventHandlers.Attach="ItemClick => ItemClick($eventArgs)">
    .....
    .....
    .....
</Grid>

患者详细信息页面

此页面允许输入新的患者详细信息,并进行以下验证:

[Export(typeof(IFluentRuleProvider<PatientDetailsPageViewModel>))]
public class FluentValidationForPatientDetailsVM : IFluentRuleProvider<PatientDetailsPageViewModel>
{
    public void ProvideRules(IFluentRuleCollection<PatientDetailsPageViewModel> collection)
    {
        collection.AddRule("PrefixRule")
                    .Property(x => x.Prefix)
                    .IsRequired()
                    .When.Property(x => x.EmailAddress)
                    .IsNotEmpty();
    }
}

[Export(typeof(IValidationMethodProvider<PatientDetailsPageViewModel>))]
public class MethodValidationForPatientDetailsVM : 
       IValidationMethodProvider<PatientDetailsPageViewModel>
{
    public void ProvideRules(IValidationMethodCollection<PatientDetailsPageViewModel> methodCollection)
    {
        methodCollection.AddRule(MinLengthRules).
            MonitorProperty(x => x.FirstName).MonitorProperty(x => x.LastName);
    }

    private void MinLengthRules(IRuleExecutionContext<PatientDetailsPageViewModel> obj)
    {
        if(!string.IsNullOrEmpty(obj.ValidationObject.FirstName))
        {
            if(obj.ValidationObject.FirstName.Length < 3)
            {
                obj.AddError(x => x.FirstName, 
                  "Your first name can't be less than 3 characters long");
                obj.Message = "The data entered is invalid";
            }
        }

        if(!string.IsNullOrEmpty(obj.ValidationObject.LastName))
        {
            if(obj.ValidationObject.FirstName.Length < 3)
            {
                obj.AddError(x => x.FirstName, "Your last name can't be less than 3 characters long");
                obj.Message = "The data entered is invalid";
            }
        }

        if (!string.IsNullOrEmpty(obj.ValidationObject.ImageFilePath))
        {
            obj.AddError(x => x.FirstName, "You must supply an image");
            obj.Message = "The data entered is invalid";
        }
    }
}

点击图片查看大图

还可以捕获网络摄像头图像,并将其存储在我们要捕获的假设患者信息中。显然,由于我们正在处理网络摄像头,我们需要编写一些代码来从它捕获图像。这是网络摄像头代码:

单击“捕获图像”按钮时会启动网络摄像头代码。

[Singleton]
[Export(typeof(IWebCamService))]
public class WebCamService : IWebCamService
{
    public IAsyncOperation<string> CaptureImageAsync()
    {
        return CaptureImageInternal().AsAsyncOperation();
    }

    private async Task<string> CaptureImageInternal()
    {
        var captureHelper = new Windows.Media.Capture.CameraCaptureUI();
        captureHelper.PhotoSettings.CroppedAspectRatio = new Size(4, 3);
        captureHelper.PhotoSettings.MaxResolution = CameraCaptureUIMaxPhotoResolution.Large3M;
        captureHelper.PhotoSettings.Format = CameraCaptureUIPhotoFormat.Png;
        IStorageFile file = await captureHelper.CaptureFileAsync(CameraCaptureUIMode.Photo);
        if (file != null)
        {
            return file.Path;
        }
        else
        {
            return null;
        }
    }
}

与所有 WinRT 事物一样,我们需要确保它使用 Async/Await。当我们单击“捕获图像”按钮时,我们将看到一个类似下图的屏幕截图,其中我们可以通过触摸或鼠标按下 OK。

点击图片查看大图

图像捕获后,我们可以选择使用标准的裁剪工具进行裁剪。

点击图片查看大图

当我们最终满意并裁剪好图像后,它将出现在我们正在捕获的患者详细信息中,大致看起来应如下所示:

点击图片查看大图

以下是此页面 ViewModel 的最相关部分:

[Syncable]
public class PatientDetailsPageViewModel : PageViewModel, IValidationStateChangedHandler
{
    ......
    ......
    ......
    ......

    [ImportConstructor]
    public PatientDetailsPageViewModel(
        ISqlLiteDatabaseService sqlLiteDatabaseService,
        IMessageBoxService messageBoxService,
        IWebCamService webCamService)
    {
        this.sqlLiteDatabaseService = sqlLiteDatabaseService;
        this.messageBoxService = messageBoxService;
        this.webCamService = webCamService;

        SaveCommand = new DelegateCommand(ExecuteSaveCommand,
                    x => ValidationContext.State == ValidationState.Valid);
        CaptureImageCommand = new DelegateCommand(ExecuteCaptureImageCommand,
                    x => ValidationContext.State == ValidationState.Valid);
    }

    [Sync]
    public string Prefix
    {
        get { return prefix; }
        set { SetProperty(ref prefix, value); }
    }

    ....
    ....
    ....


    [Import]
    public IValidationContext ValidationContext { get; set; }

    [ActivationComplete]
    public void Activated()
    {
        ValidationContext.RegisterValidationStateChangedHandler(this);
    }

    private async void ExecuteSaveCommand(object parameter)
    {
        try
        {
            PatientDetailModel patient = new PatientDetailModel();
            patient.Prefix = this.Prefix;
            patient.FirstName = this.FirstName;
            patient.LastName = this.LastName;
            patient.MiddleName = this.MiddleName;
            patient.EmailAddress = this.EmailAddress;
            patient.ImageFilePath = this.ImageFilePath;
            patient.Detail = patient.ToString();
            int id = await sqlLiteDatabaseService.SavePatientDetailAsync(patient);

            if (id > 0)
            {
                string msg = string.Format("Patient {0} {1} {2}, saved with Id : {3}", 
                             Prefix, FirstName, LastName, id);
                this.Prefix = string.Empty;
                this.FirstName = string.Empty;
                this.LastName = string.Empty;
                this.MiddleName = string.Empty;
                this.EmailAddress = string.Empty;
                this.ImageFilePath = string.Empty;
                this.HasImage = false;
                SaveCommand.RaiseCanExecuteChanged();
                CaptureImageCommand.RaiseCanExecuteChanged();
                await messageBoxService.Show(msg); 
            }
        }
        catch (Exception ex)
        {
            if (ex is InvalidOperationException)
            {
                messageBoxService.Show(ex.Message);
            }
            else
            {
                messageBoxService.Show("There was a problem save the Patient data");
            }
        }
    }

    private IStorageFile tempFile;

    private async void ExecuteCaptureImageCommand(object parameter)
    {
        try
        {
            ImageFilePath = await webCamService.CaptureImageAsync();
            HasImage = !string.IsNullOrEmpty(ImageFilePath);
        }
        catch (Exception ex)
        {
            messageBoxService.Show("There was a problem capturing the image");
        }
    }

    public void StateChanged(IValidationContext context, ValidationState validationState)
    {
        SaveCommand.RaiseCanExecuteChanged();
        CaptureImageCommand.RaiseCanExecuteChanged();
    }
}

创建预约页面

此页面的许多工作方式都相当标准,是 ViewModel 的工作方式。但是,让我们看一下每个屏幕截图的作用,然后我们可以更详细地研究驱动此页面的 ViewModel。

因此,当我们首次加载此页面时,假设您还没有任何预约,并且您已从提供的 ComboBoxes 中选择了医生和患者,您应该会看到如下屏幕截图。从这里,您可以:

  • 选择要添加预约的日期
  • 点击左侧的时间段之一,这将基于您选择的其他条件启动新预约的添加

点击图片查看大图

点击其中一个时间段后,您会看到一个区域,您可以在其中输入消息并选择体位(正面/侧面/背面)。选择好体位后,您会看到一个图像,该图像与患者的性别相匹配,并且也与请求的体位相匹配。

您还会看到一个小的蓝色和白色圆圈。它可以自由移动,可以在当前身体图像上拖动。其想法是,这可以用来精确指示患者的问题区域在哪里。稍后在查看已保存的预约时将使用此点。

点击图片查看大图

一旦预约已保存,它将显示在屏幕左侧的时间段列表中,并且几个数据输入字段和患者 ComboBox 将被清空,因为我们希望为创建的任何新预约再次选择它们,因为它们很可能针对不同的患者/问题和体位。

还可以查看和删除选定的预约。

点击图片查看大图

我认为这个页面中更令人感兴趣的事情之一是围绕当前查看的身体拖动圆圈的方式。这可以通过一个名为 InteractiveBodyControl 的自定义控件轻松实现,该控件包含一个 Canvas 和一个 styled Button。圆圈实际上是 ButtonStyle

<Canvas Margin="10" x:Name="canv">
    <Grid HorizontalAlignment="Center" 
            VerticalAlignment="Center">
        <Image x:Name="imgMaleFront" Source="../Assets/MaleFront.png" Opacity="0"/>
        <Image x:Name="imgMaleSide" Source="../Assets/MaleSide.png" Opacity="0"/>
        <Image x:Name="imgMaleBack" Source="../Assets/MaleBack.png" Opacity="0"/>
        <Image x:Name="imgFemaleFront" Source="../Assets/FemaleFront.png" Opacity="0"/>
        <Image x:Name="imgFemaleSide" Source="../Assets/FemaleSide.png" Opacity="0"/>
        <Image x:Name="imgFemaleBack" Source="../Assets/FemaleBack.png" Opacity="0"/>
    </Grid>
    <Button ManipulationMode="All" Loaded="Button_Loaded"
            ManipulationDelta="Button_ManipulationDelta"
            Width="40" Height="40">
        <Button.Template>
            <ControlTemplate>
                <Grid>
                    <Ellipse Width="40" Height="40" Fill="#ff4617B4" 
                            HorizontalAlignment="Center" VerticalAlignment="Center"/>
                    <Ellipse Width="20" Height="20" Fill="White"
                            HorizontalAlignment="Center" VerticalAlignment="Center"/>
                </Grid>
            </ControlTemplate>
        </Button.Template>
    </Button>
</Canvas>

显然,一次只显示一个身体图像(稍后会详细介绍)。我们还有处理移动圆圈的代码,我们只需要使用标准的 ManipulationDelta 事件,如下所示:

public Point BodyPoint
{
    get
    {
        return new Point(Canvas.GetLeft(button), Canvas.GetTop(button));
    }
    set
    {
        Canvas.SetLeft(button, value.X);
        Canvas.SetTop(button, value.Y);
    }
}

private void Button_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
{
    Button button = (Button)sender;

    var maxWidth = this.Width;
    var maxHeight = this.Height;

    double newX = Canvas.GetLeft(button) + e.Delta.Translation.X;
    double newY = Canvas.GetTop(button) + e.Delta.Translation.Y;

    newX = Math.Max(0, newX);
    newX = Math.Min(maxWidth, newX);

    newY = Math.Max(0, newY);
    newY = Math.Min(maxHeight, newY);

    Canvas.SetLeft(button, newX);
    Canvas.SetTop(button, newY);
}

另外值得注意的是,我们可以设置一个 Point(当查看现有预约时),并获取当前 Point,以便可以将其保存到当前预约中。

另一个有趣的部分是身体如何根据患者的性别和请求的体位进行交换。正如我们上面看到的,实际上有六张图片:

  • MaleFront.png
  • MaleSide.png
  • MaleBack.png
  • FemaleFront.png
  • FemaleSide.png
  • FemaleBack.png

所以我们需要做的就是隐藏除与患者性别和请求体位匹配的图像之外的所有图像。为此,我们知道我们需要使用 VisualStates,但如何让我们的 ViewModel 来驱动视图中的新 VisualState 选择?

因此,由于 PageViewModel 基类(我在上一篇文章中讨论过),这在 StyleMVVM 中可以轻松实现。此类使我们能够从 ViewModel 访问 View(那些认为这不对的纯粹主义者,呸)。所以这是代码:

 [Syncable]
public class CreateAppointmentsPageViewModel : PageViewModel, IValidationStateChangedHandler
{
    .....
    .....
    .....
    .....
    private void ChangeBodyBasedOnPatientAndBodyType()
    {

        if (CurrentEditMode == EditMode.CreateNew || CurrentEditMode == EditMode.ViewExisting)
        {
            if (Patient == null || Patients == null || BodyViewType == null)
                return;

            if (Patient.Prefix == "Mr")
            {
                (this.View as ICreateAppointmentsPage).GoToVisualState(
                       string.Format("Male{0}State", BodyViewType.Value));
            }
            if (Patient.Prefix == "Mrs")
            {
                (this.View as ICreateAppointmentsPage).GoToVisualState(
                       string.Format("Female{0}State", BodyViewType.Value));
            }

            ShowImage = true;

        }
        else
        {
            showImage = false;
        }
    }
    .....
    .....
    .....
    .....
}

其中重要部分是 (this.View as ICreateAppointmentsPage).GoToVisualState(string.Format("Female{0}State", BodyViewType.Value)); 。现在让我们看看这个 ViewModels 的视图。

public interface ICreateAppointmentsPage
{
    void GoToVisualState(string visualState);
    Point BodyPoint { get; set; }
}


[Export]
public sealed partial class CreateAppointmentsPage : LayoutAwarePage, ICreateAppointmentsPage
{
    InteractiveBodyControl bodyControl = null;
    private string currentBodyState;

    public CreateAppointmentsPage()
    {
        this.InitializeComponent();
        var appSearchPane = SearchPane.GetForCurrentView();
        appSearchPane.PlaceholderText = "Name or a date (dd/mm/yy)";
        this.LayoutUpdated += CreateAppointmentsPage_LayoutUpdated;
    }

    void CreateAppointmentsPage_LayoutUpdated(object sender, object e)
    {
        GoToCurrentState();
    }

    public void GoToVisualState(string visualState)
    {
        currentBodyState = visualState;
        GoToCurrentState();
    }

    private void BodyControl_Loaded(object sender, RoutedEventArgs e)
    {
        bodyControl = sender as InteractiveBodyControl;
    }

    private void GoToCurrentState()
    {
        if (currentBodyState == null || bodyControl == null)
            return;

        bodyControl.GoToVisualState(currentBodyState);
    }

    public Point BodyPoint
    {
        get
        {
            return this.bodyControl.BodyPoint;
        }
        set
        {
            this.bodyControl.BodyPoint = value;
        }
    }
}

可以看到,View 包含代码,用于指示包含的 InteractiveBodyControl 进入特定状态。它还包含用于获取/设置 InteractiveBodyControl 使用的 Point 的代码。

这是一种强大的技术,可用于从 ViewModel 执行许多视图中心的任务。PageViewModel 非常棒,也是 StyleMVVM 我最喜欢的部分之一。

此 ViewModel 的其余工作方式都很标准,因此我将不浪费时间讨论它。

查看预约页面

此页面大部分代码与我在先前文章中已发布的相同: https://codeproject.org.cn/Articles/654374/WinRT-Simple-ScheduleControl

本质上,它是一个日程控件,允许用户通过触摸/鼠标滚动预约。这是一个相当复杂的安排,对于这个特定部分,最好阅读完整的文章,因为它涉及一些动态部分。

点击图片查看大图

查看特定医生的预约页面

此页面仅允许您查看先前已为医生保存的特定预约。大部分数据是静态的,唯一的用户交互是单击白色圆圈,它将显示在创建原始预约时记录的消息。其中大部分是标准的 MVVM 代码,因此我认为对于此页面,无需进一步详细说明。

点击图片查看大图

查看统计信息

在此页面上,您可以查看有关当前存储了多少预约以及它们在给定日期内存储在哪个医生下的统计信息。

点击图片查看大图

对于此图表,我正在使用 WinRT XAML ToolKit - Data Visualization Controls(不用担心,附加的演示应用程序已包含正确的 NuGet 包)。WinRT XAML ToolKit 包括一个 PieChart,我正在使用它。

这是创建 PieChart 的 XAML:

<charting:Chart
    x:Name="PieChart"
    Title="Doctors appointments for selected Date"
    Margin="0,0">
    <charting:Chart.Series>
        <charting:PieSeries 
            IndependentValueBinding="{Binding Name}"
            DependentValueBinding="{Binding Value}"
            IsSelectionEnabled="True" />
    </charting:Chart.Series>
    <charting:Chart.LegendStyle>
        <Style TargetType="datavis:Legend">
            <Setter Property="VerticalAlignment" Value="Stretch" />
            <Setter Property="Background" Value="#444" />
            <Setter Property="ItemsPanel">
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <controls:UniformGrid Columns="1" Rows="5" />
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="TitleStyle">
                <Setter.Value>
                    <Style TargetType="datavis:Title">
                        <Setter  Property="Margin" Value="0,5,0,10" />
                        <Setter Property="FontWeight" Value="Bold" />
                        <Setter Property="HorizontalAlignment" Value="Center" />
                    </Style>
                </Setter.Value>
            </Setter>
            <Setter Property="ItemContainerStyle">
                <Setter.Value>
                    <Style TargetType="charting:LegendItem">
                        <Setter Property="Template">
                            <Setter.Value>
                                <ControlTemplate TargetType="charting:LegendItem">
                                    <Border
                                            MinWidth="200"
                                            Margin="20,10"
                                            CornerRadius="0"
                                            VerticalAlignment="Stretch"
                                            HorizontalAlignment="Stretch"
                                            Background="{Binding Background}">
                                        <datavis:Title
                                            HorizontalAlignment="Center"
                                            VerticalAlignment="Center"
                                            FontSize="24"
                                            FontWeight="Normal"
                                            FontFamily="Segeo UI"
                                            Content="{TemplateBinding Content}" />
                                    </Border>
                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                    </Style>
                </Setter.Value>
            </Setter>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="datavis:Legend">
                        <Border
                                Background="{TemplateBinding Background}"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}"
                                Padding="2">
                            <Grid>
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="Auto" />
                                    <RowDefinition />
                                </Grid.RowDefinitions>
                                <datavis:Title
                                    Grid.Row="0"
                                    x:Name="HeaderContent"
                                    Content="{TemplateBinding Header}"
                                    ContentTemplate="{TemplateBinding HeaderTemplate}"
                                    Style="{TemplateBinding TitleStyle}" />
                                <ScrollViewer
                                    Grid.Row="1"
                                    VerticalScrollBarVisibility="Auto"
                                    BorderThickness="0"
                                    Padding="0"
                                    IsTabStop="False">
                                    <ItemsPresenter
                                        x:Name="Items"
                                        Margin="10,0,10,10" />
                                </ScrollViewer>
                            </Grid>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </charting:Chart.LegendStyle>
</charting:Chart>

其中系列数据绑定到一个 IEnumerable<NameValueItemViewModel>,而 NameValueItemViewModel 看起来像这样:

public class NameValueItemViewModel
{
    public string Name { get; set; }
    public int Value { get; set; }
}

不幸的是,我似乎无法直接在 XAML 中绑定系列数据。因此,我不得不采用与之前相同的技巧(将 VisualState 从 ViewModel 设置,我们使用 PageViewModel 和 View 上的接口)。

[Export]
public sealed partial class ViewStatsPage : LayoutAwarePage, IViewStatsPage
{
    public void SetChart(List>NameValueItemViewModel> items)
    {
        ((PieSeries)this.PieChart.Series[0]).ItemsSource = items;
    }
}

与 SharePane 交互

我真正想实现的另一个事情是与 SearchPane 交互。我确实设法达到了我的目的。但我的天,WinRT 目前期望你与 SearchPane 交互的方式在我看来相当糟糕。它基本上迫使你将越来越多的代码塞入 App.xaml.cs 或将其委派给其他地方。我不知道我希望它如何工作,但我确实知道它目前的方式是纯粹的肮脏。我觉得它粗糙而淫秽。

点击图片查看大图

一切都始于 App.xaml.cs 中一些脏乱而错放的代码,如下所示:

private void ConfigureSearchContract()
{
    var appSearchPane = SearchPane.GetForCurrentView();
    appSearchPane.PlaceholderText = "Name or a date (dd/mm/yy)";
}

// <summary>
/// Invoked when the application is activated to display search results.
/// </summary>
/// <param name="args">Details about the activation request.</param>
protected async override void OnSearchActivated(
  Windows.ApplicationModel.Activation.SearchActivatedEventArgs args)
{
    ConfigureSearchContract();

    if (args.PreviousExecutionState != ApplicationExecutionState.Running)
    {
        LaunchBootStrapper();
    }

    // If the Window isn't already using Frame navigation, insert our own Frame
    var previousContent = Window.Current.Content;
    var frame = previousContent as Frame;

    // If the app does not contain a top-level frame, it is possible that this 
    // is the initial launch of the app. Typically this method and OnLaunched 
    // in App.xaml.cs can call a common method.
    if (frame == null)
    {
        // Create a Frame to act as the navigation context and associate it with
        // a SuspensionManager key
        frame = new Frame();
        SuspensionManager.RegisterFrame(frame, "AppFrame");

        if (args.PreviousExecutionState == ApplicationExecutionState.Terminated)
        {
            // Restore the saved session state only when appropriate
            try
            {
                await SuspensionManager.RestoreAsync();
            }
            catch (Exception)
            {
                //Something went wrong restoring state.
                //Assume there is no state and continue
            }
        }
    }

    frame.Navigate(typeof(SearchResultsView), args.QueryText);
    Window.Current.Content = frame;

    // Ensure the current window is active
    Window.Current.Activate();
}

其中每个完整页面视图在其代码隐藏文件中也有这个,负责在 SearchPane 的搜索 TextBox 中显示水印文本:

var appSearchPane = SearchPane.GetForCurrentView();
appSearchPane.PlaceholderText = "Name or a date (dd/mm/yy)";

点击图片查看大图

发生的情况是,当用户在 SearchPane 的 TextBox 中键入值时,会加载 SearchResultView,并且当前的搜索值作为导航参数传入,这些参数随后可以从 PageViewModel.OnNavigatedTo(..) 方法中获取,然后我们可以从那里运行搜索。如果我向您展示下面的 SearchResultViewModel,这可能会更清楚。

[Singleton]
[Syncable]
public class SearchResultsViewModel : PageViewModel
{
    private string searchString;
    private List<SearchResultViewModel> results;
    private bool hasResults = false;

    private ISqlLiteDatabaseService sqlLiteDatabaseService;
        
    [ImportConstructor]
    public SearchResultsViewModel(
        ISqlLiteDatabaseService sqlLiteDatabaseService)
    {
        this.sqlLiteDatabaseService = sqlLiteDatabaseService;
    }

    [Sync]
    public string SearchString
    {
        get { return searchString; }
        private set { SetProperty(ref searchString, value); }
    }

    [Sync]
    public List<SearchResultViewModel> Results
    {
        get { return results; }
        private set { SetProperty(ref results, value); }
    }

    [Sync]
    public bool HasResults
    {
        get { return hasResults; }
        private set { SetProperty(ref hasResults, value); }
    }

    private async Task<SearchResultViewModel> CreateScheduleItemViewModel(ScheduleItemModel model)
    {
        var doctors = await sqlLiteDatabaseService.GetDoctorsAsync;
        var doctorName = doctors.Single(x => x.DoctorId == model.DoctorId).Name;

        var patient = await sqlLiteDatabaseService.GetPatientAsync(model.PatientId);

        return new SearchResultViewModel(model.Date, patient.FullName, 
            new Time(model.StartTimeHour, model.StartTimeMinute),
            new Time(model.EndTimeHour, model.EndTimeMinute), doctorName);

    }

    protected async override void OnNavigatedTo(object sender, StyleNavigationEventArgs e)
    {
        SearchString = NavigationParameter as string;
        DoSearch(SearchString);
    }


    private async void DoSearch(string searchString)
    {
        try
        {
            DateTime date;
            Dictionary<DoctorModel, List<ScheduleItemModel>> 
              appointments = new Dictionary<DoctorModel, List<ScheduleItemModel>>();
            if (DateTime.TryParse(SearchString, out date))
            {
                appointments = await sqlLiteDatabaseService.FetchScheduleItemsAsync(date);
            }
            else
            {
                appointments = await sqlLiteDatabaseService.SearchScheduleItemsAsync(searchString);
            }

            if (appointments.Any())
            {
                CreateAppointments(appointments);
            }
            else
            {
                HasResults = false;
            }
        }
        catch
        {
            //not much we can do about it, other than set a property.
            //Would not be good form to show a messagebox using Search contract
            Results = new List<SearchResultViewModel>();
            HasResults = false;
        }
    }

    protected async void CreateAppointments(Dictionary<DoctorModel, 
                         List<ScheduleItemModel>> appointments)
    {
        List<SearchResultViewModel> localResults = new List<SearchResultViewModel>();
        foreach (KeyValuePair<DoctorModel, List<ScheduleItemModel>> appointment in appointments)
        {
            if (appointment.Value.Any())
            {
                foreach (var scheduleItemModel in appointment.Value)
                {
                    var resulVm = await CreateScheduleItemViewModel(scheduleItemModel);
                    localResults.Add(resulVm);
                }
            }
        }

        if (localResults.Any())
        {
            Results = localResults;
            HasResults = true;
        }
        else
        {
            Results = new List<SearchResultViewModel>();
            HasResults = false;
        }

    }

}

一旦我们获得结果,就只需将其绑定到某个控件,如下所示,我们只是使用带有自定义 DataTemplate 的标准 GridView 控件:

<GridView
    Visibility="{Binding HasResults, 
      Converter={StaticResource BoolToVisibilityConv}, ConverterParameter='False'}"
    x:Name="resultsGridView"
    AutomationProperties.AutomationId="ResultsGridView"
    AutomationProperties.Name="Search Results"
    Margin="20"
    TabIndex="1"
    Grid.Row="0"
    SelectionMode="None"
    IsSwipeEnabled="false"
    IsItemClickEnabled="False"
    ItemsSource="{Binding Source={StaticResource resultsViewSource}}"
    ItemTemplate="{StaticResource ResultsDataTemplate}">

    <GridView.ItemContainerStyle>
        <Style TargetType="Control">
            <Setter Property="Margin" Value="20"/>
        </Style>
    </GridView.ItemContainerStyle>
</GridView>

<DataTemplate x:Key="ResultsDataTemplate">
    <Grid HorizontalAlignment="Left" Width="Auto" 
              Height="Auto" Background="#ff4617B4">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <Image Grid.Column="0" VerticalAlignment="Center"
                Width="50" Height="50" Margin="10"
                Source="{Binding Image}"/>

        <StackPanel Grid.Column="1" Orientation="Vertical" Margin="10">
            <StackPanel Orientation="Horizontal">
                <TextBlock Foreground="CornflowerBlue"
                    FontSize="24"
                    FontFamily="Segeo UI"
                    FontWeight="Normal"
                    Text="{Binding Date}" />
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="Doctor :" Foreground="White" 
                   Style="{StaticResource SearchLabelStyle}"/>
                <TextBlock Text="{Binding DoctorName}" 
                   Foreground="White" Style="{StaticResource SearchLabelStyle}"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding StartTime}" 
                  Style="{StaticResource SearchLabelStyle}" Foreground="White" />
                <TextBlock Text="-" Foreground="White" 
                  Style="{StaticResource SearchLabelStyle}"/>
                <TextBlock Text="{Binding EndTime}" 
                  Foreground="White" Style="{StaticResource SearchLabelStyle}"/>
                <TextBlock Text=" " Foreground="White" 
                  Style="{StaticResource SearchLabelStyle}"/>
                <TextBlock Text="{Binding PatientName}" 
                  Foreground="White" Style="{StaticResource SearchLabelStyle}"/>
            </StackPanel>
        </StackPanel>
    </Grid>
</DataTemplate>

就这样

好的,以上就是对 StyleMVVM 演示应用程序的简要介绍,以及我第一次(而且我不得不说,可能是最后一次)编写的 Windows 8 完整应用程序。老实说,我并不真正享受整体体验,我认为 WinRT 需要大量工作才能使其接近有用/高效。目前它感觉非常拼凑和 hacky,并且 WPF 和 Silverlight 中的许多过去经验似乎都丢失了,这真令人遗憾。

我暂时也将避开任何 UI 工作,而是专注于学习一门新语言,那就是 F#。我想我可能会在我的旅程(字面上从零开始)中写一些关于 F# 的博客文章。

一如既往,欢迎任何投票/评论,我相信 Ian 会很感激大家试用他的宝贝(当然是 StyleMVVM)。

© . All rights reserved.