WinRT : 简单的 ScheduleControl





5.00/5 (15投票s)
在此下载 >> 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,我们可以绕过它。以下代码并非我原创,而是从互联网上找到的(手头没有链接),但它肯定基于这篇文章:
这是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
。
唯一巧妙的地方是我们隐藏了除了主中央区域之外所有 ScrollViewer
的 ScrollBar
,然后我们有一些后台代码,它不仅负责设置所有必需的 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 负责创建 HourHeaderViewModel
和 DoctorViewModel
,并且在下一篇文章中,我处理到那部分时,它也将负责设置空白项。
此 ViewModel 的工作方式相当简单,它基本上根据 SlotDurationInMins 变量(默认为 15)知道水平时间槽的大小,这意味着如果一个约会持续 1 小时,我们预计完整的小时标题将跨越 4 列(60/15 = 4)。
本质上,有了这一点点数学知识,我们就足以创建所有的 HourHeaderViewModel
和 DoctorViewModel
对象了。代码如下:
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
:由于我们将有效地渲染 ScheduleItemViewModel
或 BlankItemViewModel
,我们需要一种方法来选择要应用的模板。这个 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; }
}
就这些
好了,暂时就这些了。我想我得赶紧为伊恩写那些东西了,所以我走了。一如既往,如果您喜欢这篇文章,并乐意投票、评论,那将非常受欢迎。