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

WinRT : 简单的 ScheduleControl

starIconstarIconstarIconstarIconstarIcon

5.00/5 (15投票s)

2013年9月16日

CPOL

10分钟阅读

viewsIcon

50694

downloadIcon

436

 

       在此下载 >> Windows 应用商店只读日程安排控件演示项目

 

引言

正如你们中一些人可能知道的,我曾经是WPF的忠实粉丝。突然间,出现了一个新面孔,WinRT和Windows应用商店应用程序。到目前为止,我还没有涉足这个领域,因为我认为WinRT还处于早期阶段,并且我预计它会有很大的变化。然而,我确实很喜欢它的一些外观和感觉元素,心想,何不快速试一下呢?我还与一位优秀的美国佬(嘿,伊恩)共同撰写了一篇文章,他联系我审查他的WinRT MVVM框架,名为:StyleMVVM,我答应伊恩我会研究并试用一下。

问题是,我不想在没有至少在小项目上尝试WinRT的情况下,就启动一个大型WinRT应用程序(我喜欢编写完整的应用程序,因为那样往往能学到更多)。我仍然完全打算制作一个更大的StyleMVVM应用程序,事实上,本文中介绍的材料将构成我答应伊恩的更大的StyleMVVM演示应用程序的一部分。

此演示附带的代码非常简单,但实际上,对于一个WPF开发者来说,它足以尝试并找出WinRT的开发方式,我不得不说,从WPF的角度来看,确实有一些奇怪的事情是意想不到的。我将在文章正文中讨论这些内容,但我们跑题了,演示应用程序是做什么的呢?

就像我说的,我想把事情保持得非常简单,所以我写了一个非常简单的基于网格的日程安排控件,它是只读的,你不能通过点击添加项目,所有设置都通过现有代码完成。当然,数据可以从数据库中获取,但在运行时无法通过触摸或鼠标动态添加约会,尽管这可能会在更完整的StyleMVVM应用程序中出现。

总而言之,附带的演示代码是一个简单的日程安排控件,支持触摸或鼠标。下面是屏幕截图

红色的箭头不是它的一部分,它们只是我向你展示如何拖动控件。

点击图片查看大图

您还可以点击左侧的项目与它们进行交互。在此演示中,所发生的一切都是为点击的项目显示一个消息对话框。完整StyleMVVM应用程序的计划是导航到一个新框架,您可以在其中为点击的项目添加/编辑约会。

点击图片查看大图

 

Windows 应用商店应用程序快速概述

如我所说,我对WinRT/Windows 应用商店应用程序完全陌生,所以我希望这部分内容是正确的(我可能不正确,如果我不正确,请随时纠正我)。

清单

在构建Windows 应用商店应用程序时,您需要决定的事情之一是它具有哪些功能。这些类型的事情通过一个清单文件进行管理。此演示应用程序的示例如下所示

点击图片查看大图

这个UI实际上只是表面功夫,例如,如果我们将清单文件(Package.appxmanifest)拖到Notepad.exe中,它看起来像这样

<?xml version="1.0" encoding="utf-8"?>
<Package xmlns="http://schemas.microsoft.com/appx/2010/manifest">
  <Identity Name="8663da95-c270-4c66-b5b7-28caabdcf5f3" Publisher="CN=Sacha" Version="1.0.0.0" />
  <Properties>
    <DisplayName>ScheduleControl</DisplayName>
    <PublisherDisplayName>Sacha</PublisherDisplayName>
    <Logo>Assets\StoreLogo.png</Logo>
  </Properties>
  <Prerequisites>
    <OSMinVersion>6.2.1</OSMinVersion>
    <OSMaxVersionTested>6.2.1</OSMaxVersionTested>
  </Prerequisites>
  <Resources>
    <Resource Language="x-generate" />
  </Resources>
  <Applications>
    <Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="ScheduleControl.App">
      <VisualElements DisplayName="ScheduleControl" 
		Logo="Assets\Logo.png" 
		SmallLogo="Assets\SmallLogo.png" 
		Description="ScheduleControl" 
		ForegroundText="light" 
		BackgroundColor="#464646" 
		ToastCapable="false">
        <DefaultTile ShowName="allLogos" />
        <SplashScreen Image="Assets\SplashScreen.png" />
        <InitialRotationPreference>
          <Rotation Preference="portrait" />
          <Rotation Preference="landscape" />
          <Rotation Preference="portraitFlipped" />
          <Rotation Preference="landscapeFlipped" />
        </InitialRotationPreference>
      </VisualElements>
    </Application>
  </Applications>
</Package>

您可以在清单中指定的内容包括:

  • Logo
  • 您的应用程序支持哪种屏幕旋转
  • 默认文化
  • 功能
  • 包信息

 

Logo

Windows 8支持各种不同大小的徽标,要使用的徽标在刚才看到的应用程序清单中指定。就存储位置而言,它们位于 **\Assets** 文件夹中,对于演示应用程序来说,它看起来是这样的

这还包括一个启动徽标,它将在应用程序加载时显示。

 

标准样式

Windows 应用商店应用程序附带一套标准的控件模板和样式,这些模板和样式与您的应用程序捆绑在一起。您可以随意更改它们,但后果自负。它们都包含在 **\Common** 文件夹中名为“StandardStyles.xaml”的单个文件中。

 

应用程序启动

App.Xaml 后台代码是您的 Windows 应用商店应用程序启动的地方。以下是 App.Xaml 的典型布局:

sealed partial class App : Application
{
    /// <summary>
    /// Initializes the singleton application object.  This is the first line of authored code
    /// executed, and as such is the logical equivalent of main() or WinMain().
    /// </summary>
    public App()
    {
        this.InitializeComponent();
        this.Suspending += OnSuspending;
    }

    /// <summary>
    /// Invoked when the application is launched normally by the end user.  Other entry points
    /// will be used when the application is launched to open a specific file, to display
    /// search results, and so forth.
    /// </summary>
    /// <param name="args">Details about the launch request and process.</param>
    protected override void OnLaunched(LaunchActivatedEventArgs args)
    {
        Frame rootFrame = Window.Current.Content as Frame;

        // Do not repeat app initialization when the Window already has content,
        // just ensure that the window is active
        if (rootFrame == null)
        {
            // Create a Frame to act as the navigation context and navigate to the first page
            rootFrame = new Frame();

            if (args.PreviousExecutionState == ApplicationExecutionState.Terminated)
            {
                //TODO: Load state from previously suspended application
            }

            // Place the frame in the current Window
            Window.Current.Content = rootFrame;
        }

        if (rootFrame.Content == null)
        {
            // When the navigation stack isn't restored navigate to the first page,
            // configuring the new page by passing required information as a navigation
            // parameter
            if (!rootFrame.Navigate(typeof(MainPage), args.Arguments))
            {
                throw new Exception("Failed to create initial page");
            }
        }
        // Ensure the current window is active
        Window.Current.Activate();
    }

    /// <summary>
    /// Invoked when application execution is being suspended.  Application state is saved
    /// without knowing whether the application will be terminated or resumed with the contents
    /// of memory still intact.
    /// </summary>
    /// <param name="sender">The source of the suspend request.</param>
    /// <param name="e">Details about the suspend request.</param>
    private void OnSuspending(object sender, SuspendingEventArgs e)
    {
        var deferral = e.SuspendingOperation.GetDeferral();
        //TODO: Save application state and stop any background activity
        deferral.Complete();
    }
}

这不是我的代码,它实际上只是创建新的 Windows 应用商店应用程序时获得的标准 Microsoft 样板代码。以上突出显示的关键点如下:

  •  OnLaunched 方法已挂钩,也是我们导航到当前窗口 Frame 内根页面的地方。在本例中,它是 MainPage(默认)。
  • OnSuspending 方法已挂钩,通常在这里保存任何应用程序特定的状态

 

主页 / 导航

Windows 应用商店应用程序默认使用基于 Frame 的导航。基于 Frame 的导航意味着当前内容将被新内容替换。基于 Frame 的导航的优点是您可以自动获得日志式历史记录的支持,其中返回按钮将导航回上一个内容。

 

演示应用

本节将讨论演示应用程序,如我所说,它是一个非常简单的 Windows 应用商店日程安排控件(目前是只读的)。

从WPF开发者角度看当前Windows 应用商店的不足

在我们深入探讨演示应用程序的工作原理之前,我想谈谈我在开发我的第一个 Windows 应用商店演示时发现的一些难以处理的问题。我认为造成这种情况的原因很大程度上是微软更多地基于 Silverlight 模型而不是 WPF 模型来构建 WinRT。这是否是一件好事,我目前无法下定论,但我可以说,在我看来,我不知道他们为什么不包含 WPF 和 Silverlight 中都可用的优秀强大功能,这是我无法理解的。工作已经完成,他们为什么不利用他们现有的知识呢……这很奇怪。

样式 Setter 中不允许绑定

这一个简直太奇怪了,它在WPF中已经存在了很长时间,并最终进入了Silverlight 5。那么为什么它会缺失呢?

解决方法

幸运的是,由于其他人使用Silverlight的经验并将其应用于WinRT,我们可以绕过它。以下代码并非我原创,而是从互联网上找到的(手头没有链接),但它肯定基于这篇文章:

http://blogs.msdn.com/b/delay/archive/2010/11/10/the-taming-of-the-phone-new-settervaluebindinghelper-sample-demonstrates-its-usefulness-on-windows-phone-7-and-silverlight-4.aspx

这是WinRT就绪版本,它创建了一个名为 SetterValueBindingHelper 的类,我们可以使用它。

通过使用这个辅助类,我们现在可以非常愉快地在样式内绑定 Setter,就像这样

<!-- Grid based ListBoxItem style -->
<Style x:Key="rowAndColumnBoundContainerStyle" TargetType="ListBoxItem">
    <Setter Property="helpers:SetterValueBindingHelper.PropertyBinding">
        <Setter.Value>
            <helpers:SetterValueBindingHelper>
                <helpers:SetterValueBindingHelper
                    Type="Grid"
                    Property="Column"
                    Binding="{Binding Column}" />
                <helpers:SetterValueBindingHelper
                    Type="Grid"
                    Property="ColumnSpan"
                    Binding="{Binding ColumnSpan}" />
                <helpers:SetterValueBindingHelper
                    Type="Grid"
                    Property="Row"
                    Binding="{Binding Row}" />
                <helpers:SetterValueBindingHelper
                    Type="Grid"
                    Property="RowSpan"
                    Binding="{Binding RowSpan}" />
            </helpers:SetterValueBindingHelper>
        </Setter.Value>
    </Setter>
......
......
......
......
</Style>

 

没有 DependencyPropertyDescriptor 类

作为WPF开发者,我习惯于能够使用 DependencyPropertyDescriptor 类来挂钩 DependencyProperty 的更改。WinRT没有这个。多年来,人们在 Silverlight 中使用了各种技巧来实现类似的功能,例如 Anoop 的这个:

http://www.amazedsaint.com/2009/12/silverlight-listening-to-dependency.html

 

解决方法

使用我在这个博客上找到的一些巧妙的 FrameworkElement 扩展方法

http://blogs.interknowlogy.com/2012/11/28/dpchangedwinrt/

这现在允许我们像这样实现我们想要的功能

dataItemsScroller.RegisterDependencyPropertyChanged(() => dataItemsScroller.VerticalOffset, VerticalOffsetChangedHandler);

private void VerticalOffsetChangedHandler(DependencyPropertyChangedEventArgs obj)
{
    doctorsOnDutyScroller.ScrollToVerticalOffset(dataItemsScroller.VerticalOffset);
}

我确信这些问题以及我小范围尝试中尚未发现的更多问题,都将在后续版本中得到解决,但目前这些替代方案运作良好。向这两个辅助类的原创作者致敬。

 

演示应用的功能

正如我所说的,目前附带的演示应用程序功能不多,因为我只是想在开始更大的项目之前稍微玩一下WinRT。下面列出了演示应用程序的功能:

  • 从内存中读取仓库以获取模拟的约会
  • 在模拟的内存约会之上显示一个只读的日程安排
  • 允许用户使用触摸/鼠标上下平移
  • 允许用户点击右侧的一个项目,此时将显示一个对话框窗口

正如我所说,长远目标是允许用户创建/编辑约会。这足以快速上手 WinRT

 

它是如何工作的

它的核心是一个非常简单的控件,有3个 ScrollViewer

  • 一个在顶部用于时间(我的情况是小时)
  • 一个在左侧代表资源(我的情况是医生)
  • 一个居中的是约会

这些基本上是使用标准的 Grid 控件布局的,我们把内容放在 Row(s)/Column(s) 中,并根据约会占用的时间调整 ColumnSpan

唯一巧妙的地方是我们隐藏了除了主中央区域之外所有 ScrollViewerScrollBar,然后我们有一些后台代码,它不仅负责设置所有必需的 Grid Row(s)/Column(s),还负责设置不同 ScrollViewer 之间的交互。这在下面显示

public sealed partial class ScheduleView : UserControl
{
    private Grid hoursGrid;
    private Grid scheduleGrid;
    private ScheduleViewModel scheduleViewModel = null;

    public ScheduleView()
    {
        this.InitializeComponent();

        dataItemsScroller.RegisterDependencyPropertyChanged(() => dataItemsScroller.VerticalOffset, 
            VerticalOffsetChangedHandler);
        dataItemsScroller.RegisterDependencyPropertyChanged(() => dataItemsScroller.HorizontalOffset, 
            HorizontalOffsetChangedHandler);

        doctorsOnDutyScroller.RegisterDependencyPropertyChanged(() => doctorsOnDutyScroller.VerticalOffset, 
            DoctorsVerticalOffsetChangedHandler);

        scheduleList.RegisterDependencyPropertyChanged(() => scheduleList.DataContext, 
            DataContextChangeHandler);
    }

    private void DataContextChangeHandler(DependencyPropertyChangedEventArgs obj)
    {
        if (obj.NewValue == null)
            return;

        scheduleViewModel = (ScheduleViewModel)obj.NewValue;

        if (hoursGrid != null)
            SetupHoursGrid();

        if (scheduleGrid != null)
            SetupScheduleGrid();

    }

    private void VerticalOffsetChangedHandler(DependencyPropertyChangedEventArgs obj)
    {
        doctorsOnDutyScroller.ScrollToVerticalOffset(dataItemsScroller.VerticalOffset);
    }
        
    private void DoctorsVerticalOffsetChangedHandler(DependencyPropertyChangedEventArgs obj)
    {
        dataItemsScroller.ScrollToVerticalOffset(doctorsOnDutyScroller.VerticalOffset);
    }

    private void HorizontalOffsetChangedHandler(DependencyPropertyChangedEventArgs obj)
    {
        hoursScroller.ScrollToHorizontalOffset(dataItemsScroller.HorizontalOffset);
    }

    private void HoursGrid_Loaded(object sender, RoutedEventArgs e)
    {
        scheduleViewModel = this.DataContext as ScheduleViewModel;
        hoursGrid = sender as Grid;
        SetupHoursGrid();
    }

    private void ScheduleGrid_Loaded(object sender, RoutedEventArgs e)
    {
        scheduleViewModel = this.DataContext as ScheduleViewModel;
        scheduleGrid = sender as Grid;
        SetupScheduleGrid();
    }

    private void SetupScheduleGrid()
    {
        if (scheduleViewModel == null)
            return;

        int numberOfRows = scheduleViewModel.DoctorAppointments.Count;

        scheduleGrid.RowDefinitions.Clear();

        for (int i = 0; i < numberOfRows; i++)
        {
            scheduleGrid.RowDefinitions.Add(new RowDefinition()
            {
                Height = new GridLength(ScheduleViewModel.SlotHeight, GridUnitType.Pixel)
            });
        }

        SetupHoursCapableGrid(scheduleGrid);

    }


    private void SetupHoursGrid()
    {
        SetupHoursCapableGrid(hoursGrid);
    }


    private void SetupHoursCapableGrid(Grid grid)
    {
        int numberOfColumns = (ScheduleViewModel.MinutesInHour / ScheduleViewModel.SlotDurationInMins) * 
            ScheduleViewModel.NumberOfHours;

        grid.ColumnDefinitions.Clear();

        for (int i = 0; i < numberOfColumns; i++)
        {
            grid.ColumnDefinitions.Add(new ColumnDefinition()
            {
                Width = new GridLength(ScheduleViewModel.SlotWidth, GridUnitType.Pixel)
            });
        }

    }
}

这张图可能有助于进一步描述这一点

点击图片查看大图

 

主要类

内存中仓库

这是一个简单的服务,它分发模拟数据,这些数据在实际生活中显然会来自数据库

public interface IAppointmentProvider
{
    Dictionary<DoctorModel, List<ScheduleItemModel>> GetDoctorApppointments();
}

public class AppointmentProvider : IAppointmentProvider
{
    public Dictionary<DoctorModel, List<ScheduleItemModel>> GetDoctorApppointments()
    {
            //setup days (left hand side column)
        Dictionary<DoctorModel, List<ScheduleItemModel>> data = 
		new Dictionary<DoctorModel, List<ScheduleItemModel>>();

        DoctorModel doctorModel = new DoctorModel(1, "Dr John Smith");
        List<ScheduleItemModel> appointments = new List<ScheduleItemModel>();
        appointments.Add(new ScheduleItemModel(Time.Parse("08:00"), Time.Parse("09:30"), 
		"08:00-09:30", doctorModel.DoctorId));
        appointments.Add(new ScheduleItemModel(Time.Parse("14:00"), Time.Parse("17:00"), 
		"14:00-17:00", doctorModel.DoctorId));
        data.Add(doctorModel, appointments);

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

            
        data.OrderBy(x => x.Key.DoctorId);
        return data;

    }

    private void AddDummyAppointment(DoctorModel doctorModel, Dictionary<DoctorModel, 
	List<ScheduleItemModel>> data)
    {
        List<ScheduleItemModel> appointments = new List<ScheduleItemModel>();
        appointments.Add(new ScheduleItemModel(Time.Parse("05:00"), Time.Parse("12:00"), 
		"05:00-12:00", doctorModel.DoctorId));
        appointments.Add(new ScheduleItemModel(Time.Parse("00:30"), Time.Parse("04:30"), 
		"00:30-04:30", doctorModel.DoctorId));
        appointments.Add(new ScheduleItemModel(Time.Parse("16:30"), Time.Parse("18:00"), 
		"16:30-18:00", doctorModel.DoctorId));
        data.Add(doctorModel, appointments);
    }
}

HourHeaderViewModel

没有某种时间标题,任何日程安排控件都无法工作,这个 ScheduleControl 也不例外。这是我的 HourHeaderViewModel

[DebuggerDisplay("{DisplayText}")]
public class HourHeaderViewModel : INPCBase
{
    public HourHeaderViewModel(int hour, int column, int columnSpan)
    {
        this.DisplayText = Utils.GetCorrectedString(hour);
        this.ColumnSpan = columnSpan;
        this.Column = column;
        this.Row = 0;
    }

    public string DisplayText { get; set; }
    public int ColumnSpan { get; set; }
    public int Column { get; set; }
    public int Row { get; set; }
}

它在 XAML 中创建如下:

<!-- Hours -->
<ScrollViewer x:Name="hoursScroller" ZoomMode="Disabled" Grid.Row="0" Grid.Column="1" 
                IsEnabled="False"
                VerticalScrollBarVisibility="Hidden" HorizontalScrollBarVisibility="Hidden" >
        <ListBox BorderThickness="0"
                    HorizontalAlignment="Stretch"
                    ItemsSource="{Binding HourHeaders}"
                    ItemContainerStyle="{StaticResource rowAndColumnBoundContainerStyle}"
                    ItemTemplate="{StaticResource HourItemTemplate}"
                    IsHitTestVisible="False">
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <Grid Background="White" Loaded="HoursGrid_Loaded" >
                    </Grid>
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
        </ListBox>
</ScrollViewer>

 

DoctorViewModel

正如您可能看出的那样,DoctorViewModel 是一个非常简单的 ViewModel,用于表示 ScheduleControl 的左侧。以下是 DoctorViewModel 的代码,其中没有非常复杂的内容

[DebuggerDisplay("{DoctorName}")]
public class DoctorViewModel : IEquatable<DoctorViewModel>
{
    public DoctorViewModel(int doctorId, string doctorName)
    {
        this.DoctorId = doctorId;
        this.DoctorName = doctorName;
        SlotHeight = ScheduleViewModel.SlotHeight;
    }

    public double SlotHeight { get; private set; }
    public int DoctorId { get; private set; }
    public string DoctorName { get; private set; }

    public override int GetHashCode()
    {
        return DoctorId;
    }

    public override bool Equals(object obj)
    {
        if (obj.GetType() != this.GetType())
            return false;

        return (Equals((DoctorViewModel)obj));
    }

    public bool Equals(DoctorViewModel other)
    {
        if (other == null)
            return false;


        if (Object.ReferenceEquals(this, other))
            return true;

        if ((other as DoctorViewModel).DoctorId == this.DoctorId)
            return true;

        return false;
    }
}

这是处理渲染 DoctorViewModel 的主要 XAML,同样,没什么特别的

<!-- DoctorsOnDuty -->
<ScrollViewer  x:Name="doctorsOnDutyScroller" ZoomMode="Disabled" Grid.Row="1" Grid.Column="0" 
                IsEnabled="True"
                HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden">
    <ItemsControl 
        Background="White"
        IsHitTestVisible="True"
        Height="{Binding MaxHeight}"
        ItemsSource="{Binding DoctorsOnDuty}">
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel Orientation="Vertical"/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>

        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Border Height="{Binding SlotHeight}" 
                        VerticalAlignment="Center">
                    <StackPanel Orientation="Horizontal" Margin="2">
                        <Button Width="Auto" Height="Auto" Margin="2" 
                                Style="{StaticResource HyperLinkButtonStyle}"
                                Content="{Binding DoctorName}"
                                Command="{Binding ElementName=scheduleView, 
                                    Path=DataContext.NavigateToAppointmentsDetailCommand}"
                                CommandParameter="{Binding DoctorName}">
                        </Button>
                    </StackPanel>
                </Border>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</ScrollViewer>

ScheduleItemViewModel

这可能是整个演示应用程序中唯一稍微复杂的部分(至少是我编写的部分)。这个 ViewModel 负责创建 HourHeaderViewModelDoctorViewModel,并且在下一篇文章中,我处理到那部分时,它也将负责设置空白项。

此 ViewModel 的工作方式相当简单,它基本上根据 SlotDurationInMins 变量(默认为 15)知道水平时间槽的大小,这意味着如果一个约会持续 1 小时,我们预计完整的小时标题将跨越 4 列(60/15 = 4)。

本质上,有了这一点点数学知识,我们就足以创建所有的 HourHeaderViewModelDoctorViewModel 对象了。代码如下:

public class ScheduleViewModel : IScheduleViewModel
{
    private IMessageBoxService messageBoxService;
    private IAppointmentProvider appointmentProvider;

    public static int StartHour = 0;
    public static int EndHour = 24;
    public static int SlotDurationInMins = 15;
    public static readonly int MinutesInHour = 60;
    public static readonly double SlotHeight = 60;
    public static readonly double SlotWidth = 50;


    public ScheduleViewModel(IAppointmentProvider appointmentProvider, IMessageBoxService messageBoxService)
    {
        this.messageBoxService = messageBoxService;
        this.appointmentProvider = appointmentProvider;
        SetupAll();
        MaxWidth = ((MinutesInHour / SlotDurationInMins) * SlotWidth) * NumberOfHours;
        MaxHeight = DoctorsOnDuty.Count * SlotHeight;
        NavigateToAppointmentsDetailCommand = new DelegateCommand(x =>
        {
            messageBoxService.ShowMessage(string.Format("This would navigate to full screen appointments for doctor '{0}'", x));   
        });
    }

    public double MaxWidth { get; set; }
    public double MaxHeight { get; set; }
    public List<HourHeaderViewModel> HourHeaders { get; private set; }
    public List<DoctorViewModel> DoctorsOnDuty { get; private set; }
    public ObservableCollection<TimeItemViewModelBase> DoctorAppointments { get; private set; }
    public ICommand NavigateToAppointmentsDetailCommand { get; private set; }

    public static int NumberOfColumns
    {
        get { return (MinutesInHour / SlotDurationInMins) * NumberOfHours; }
    }

    public static int NumberOfHours
    {
        get { return EndHour - StartHour; }
    }

    public void AddNewScheduleItem(int doctorId, Time startTime, Time endTime, string message)
    {
        //not needed for this type of VM
    }

    public void RemoveScheduleItem(ScheduleItemViewModel scheduleItem)
    {
        //not needed for this type of VM
    }

    private void SetupAll()
    {
        this.DoctorAppointments = new ObservableCollection<TimeItemViewModelBase>();
        var dummyData = appointmentProvider.GetDoctorApppointments();

        HourHeaders = new List<HourHeaderViewModel>();
        DoctorsOnDuty = dummyData.Keys.Select(x => CreateDoctorViewModel(x)).ToList();
        SetupHours();
        SetupStoredScheduleItems(dummyData.Values.ToList());
    }

    private void SetupStoredScheduleItems(List<List<ScheduleItemModel>> storedScheduleModels)
    {
        int rowNumber = 0;
        foreach (List<ScheduleItemModel> scheduleItemModels in storedScheduleModels)
        {
            foreach (ScheduleItemModel scheduleItemModel in scheduleItemModels)
            {
                ScheduleItemViewModel scheduleItemViewModel = CreateScheduleItemViewModel(scheduleItemModel, rowNumber);
                this.DoctorAppointments.Add(scheduleItemViewModel);
            }
            rowNumber++;
        }
    }

    private void SetupHours()
    {
        //setup hour headers (top strip)
        int column = 0;
        int columnSpan = (MinutesInHour / SlotDurationInMins);
        for (int hour = StartHour; hour < EndHour; hour++)
        {

            HourHeaders.Add(new HourHeaderViewModel(hour, column, columnSpan));
            column += columnSpan;
        }
    }

    private DoctorViewModel CreateDoctorViewModel(DoctorModel model)
    {
        return new DoctorViewModel(model.DoctorId, model.DoctorName);
    }

    private ScheduleItemViewModel CreateScheduleItemViewModel(ScheduleItemModel model, int rowNumber)
    {
        int column = GetStartColumnFromStartTime(model.StartTime);
        int endColumn = GetEndColumnFromEndTime(model.EndTime);
        int columnSpan = endColumn - column;
        return new ScheduleItemViewModel(messageBoxService, this, column, columnSpan, rowNumber, 1, 
            model.StartTime, model.EndTime, model.Message, model.DoctorId);
    }

    private int GetStartColumnFromStartTime(Time startTime)
    {
        int startFullHourDiff = Math.Abs(StartHour - startTime.Hour);
        int columnOffSet = (MinutesInHour / SlotDurationInMins) * startFullHourDiff;
        int minColumnsOffSet = (startTime.Minute / SlotDurationInMins);
        columnOffSet += minColumnsOffSet;
        return columnOffSet;
    }

    private int GetEndColumnFromEndTime(Time endTime)
    {
        int startFullHourDiff = Math.Abs(StartHour - endTime.Hour);
        int columnOffSet = (MinutesInHour / SlotDurationInMins) * startFullHourDiff;
        int minColsOffSet = 0;
        if (endTime.Minute == 59)
        {
            minColsOffSet = (60 / SlotDurationInMins);
        }
        else
        {
            minColsOffSet = (endTime.Minute / SlotDurationInMins);
        }
        columnOffSet += minColsOffSet;
        return columnOffSet;
    }
}

我们有以下 XAML 来表示日程安排项。

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="45"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>


    <!-- Hours -->
    <ScrollViewer x:Name="hoursScroller" ZoomMode="Disabled" Grid.Row="0" Grid.Column="1" 
                    IsEnabled="False"
                    VerticalScrollBarVisibility="Hidden" HorizontalScrollBarVisibility="Hidden" >
            <ListBox.../>
    </ScrollViewer>

    <!-- DoctorsOnDuty -->
    <ScrollViewer  x:Name="doctorsOnDutyScroller" ZoomMode="Disabled" Grid.Row="1" Grid.Column="0" 
                    IsEnabled="True"
                    HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden">
        <ItemsControl.../> 
    </ScrollViewer>

    <ScrollViewer  x:Name="dataItemsScroller" ZoomMode="Disabled" Grid.Row="1" Grid.Column="1" 
                    HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
                    ManipulationMode="None" BorderThickness="0">
        <ListBox x:Name="scheduleList"
                BorderThickness="0"
                IsDoubleTapEnabled="True"
                ItemTemplateSelector="{StaticResource scheduleItemTemplateSelector}"
                HorizontalAlignment="Stretch"
                Height="{Binding MaxHeight}"
                Width="{Binding MaxWidth}"
                ItemsSource="{Binding DoctorAppointments}"
                ItemContainerStyle="{StaticResource rowAndColumnBoundContainerStyle}"
                IsHitTestVisible="False">
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <Grid Background="White" Loaded="ScheduleGrid_Loaded" IsDoubleTapEnabled="True">
                    </Grid>
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
        </ListBox>
        </ScrollViewer>

</Grid>

 

未来工作

当我将此代码移植到更完整的 StyleMVVM 应用程序时,我希望通过点击空白槽位、检查与其他约会的冲突等,使日程安排控件可编辑。因此,我保留了一些我知道将需要的代码,即这些类:

BlankItemViewModel:这将代表一个可点击的空白时间段,届时将启动某种 UI,允许将当前点击的时间段(可能更多)填充新的预约。想象一下 Outlook 的工作方式。这还将检查下一个/先前使用的时间段,并确保新预约不会与它们重叠。

ScheduleItemTemplateSelector:由于我们将有效地渲染 ScheduleItemViewModelBlankItemViewModel,我们需要一种方法来选择要应用的模板。这个 ScheduleItemTemplateSelector 代码就是做这个的。出于好奇,这是 ScheduleItemTemplateSelector 代码。

public class ScheduleItemTemplateSelector : DataTemplateSelector
{
    protected override Windows.UI.Xaml.DataTemplate SelectTemplateCore(object item, Windows.UI.Xaml.DependencyObject container)
    {
        if (item is BlankItemViewModel)
        {
            return BlankTemplate;
        }

        if (item is ScheduleItemViewModel)
        {
            return ScheduleItemTemplate;
        }

        return null;
    }

    public DataTemplate BlankTemplate { get; set; }
    public DataTemplate ScheduleItemTemplate { get; set; }
}

就这些

好了,暂时就这些了。我想我得赶紧为伊恩写那些东西了,所以我走了。一如既往,如果您喜欢这篇文章,并乐意投票、评论,那将非常受欢迎。

© . All rights reserved.