WinRT : Simple ScheduleControl





5.00/5 (15 votes)
Download Here >> Windows store read only schedule control demo project
Introduction
As some of you may know I have been somewhat of a fan boy of WPF. All of a sudden there is this new kid on the block, WinRT and Windows store applications. So far. I have not ventured into this world, as I think it is early days with WinRT, and I expect it to change a lot. I did however quite like some of the look and feel elements of it, and thought what the heck it can't hurt to take it for a quick spin. I also just co-authored an article with a fine yanky chap (hey Ian) who contacted me to review his WinRT MVVM framework, called : StyleMVVM which I promised Ian I would look at, and also take for a spin.
Thing is I did not want to start a big WinRT application (I like to write full apps as you tend to learn a lot more that way), without at least trying out WinRT on something smaller. I still fully intend on doing a larger StyleMVVM app, in fact the material presented in this article will form part of the larger StyleMVVM demo application that I promised Ian.
The code attached to this demo is pretty simple, but actually is was enough for a WPF guy to try and find out the WinRT way of doing things, and I have to say there certainly were a few weird things that were quite unexpected coming from WPF land. I will talk about these in the body of the article, but we digress, what does the demo app do?
Like I say I wanted to keep things very simple, so I have written a very simple grid based schedule control, that is readonly you can not add items to it by clicking, its all setup via existing code. Ok that data could suck stuff from a database, but there is no way to dynamically add appointments to the schedule using touch, or the mouse at runtime, though that may feature in the fuller StyleMVVM app.
So in summary, the attached demo code is a simple schedule control that works with touch or the mouse. Here is a screen shot
The red arrows are not part of it, they are me just showing you how you can drag the control around.
CLICK IMAGE FOR LARGER VERSION
You can also click on the left hand side items to interact with them. In this demo all that happens is that a message dialog is shown for the clicked item. The plan for the full StyleMVVM app would be to navigate to a new frame where you could add/edit appointments for the clicked item.
CLICK IMAGE FOR LARGER VERSION
Quick Windows Store Application Overview
As I stated I am brand new to WinRT/Windows Store apps, so I hope I get this section right (I may not, and if I don't please feel free to pull me up on it).
Manifest
One of the things you need to decide when building your Windows Store app is what capabilities it has. These sorts of things are managed via a single manifest file. An example for this demo application can be seen below:
CLICK IMAGE FOR LARGER VERSION
This UI is really just gloss, for example if we drag the manifest file (Package.appxmanifest) into Notepad.exe here is what it looks like:
<?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>
The sort of things you can specify in the manifest are:
- Logo
- What screen rotation your app supports
- The default culture
- Capabilities
- Package information
Logo
Windows 8 supports various different size logos, the one to use are specified in the app manifest we just saw. In terms of where they are stored they are available in the \Assets folder, which for the demo app looks like this:
This also includes a splash logo that will be shown while the app loads.
Standard Styles
Windows Store apps come with a standard set of control Template(s) and Style(s) which come bundled with your app. You are free to change these but it is at your peril. They are all contained in a single file called "StandardStyles.xaml" in the \Common folder.
App Startup
App.Xaml code behind is where your windows store app get brought to life. Here is a typical arrangement of what App.Xaml may look like:
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();
}
}
This is not my code, it is in fact just the standard Microsoft boiler plate code that you get when you create a new Windows Store application. The salient points above are as follows:
- The
OnLaunched
method is hooked up, and it is also where we navigate to the root page within the current windowsFrame
. Which in this case inMainPage
(the default) - The
OnSuspending
method us hookup up, this is where one would typically save any application specific state
Main Page / Navigation
Windows Store applictaions by default use Frame
based
navigation. Frame based navigation means that the current content will be
swapped out for the new content. The beauty of Frame
based
navigation is that you get automatic support for a journal type history, where
the back button would navigate back to the previous content.
Demo App
This section will discuss the demo application, which as I say is a very simple Windows Store schedule control (which at present is read only).
Current Windows Store Shortcomings From A WPF Developer Point Of View
Before we get into the nitty gritty of how the demo app works I just wanted to talk about a few things that I found quite hard to deal with when developing my 1st Windows Store demo. I think the reason for a lot of this is the fact that Microsoft based WinRT more on the Silverlight model than the WPF model. Whether that is a good thing or not I can not say yet, what I can say is that in my opinion I do not know why they did not include excellent powerful features that are available in both WPF and Silverlight, that I do NOT get. The work is already done, why would they not use their existing knowledge here.....Weird.
Bindings Not Allowed In Style Setters
This one is plain weird, as it's been around in WPF for ever, and finally made its way into Silverlight 5.So why is this missing.
THE WORK AROUND
Luckily we can work around it thanks to previous pain that others went through with Silverlight and have adapted for WinRT. The following code is not my own and was found on the internet (don't have link to hand) but it was certainly based on this post :
Here is the WinRT ready version creates a class that we can use called SetterValueBindingHelper
.
By using this helper we are now able to bind Setters within Styles quite happily like this:
<!-- 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>
No DependencyPropertyDescriptor Class
Coming from WPF I was used to be able to hook up to DependencyProperty changes using the DependencyPropertyDescriptor class. WinRT doesn't have one. Various tricks have been used to achieve something similat in Silverlight over the years, such as this one from Anoop:
http://www.amazedsaint.com/2009/12/silverlight-listening-to-dependency.html
THE WORK AROUND
Use some nifty FrameworkElement extension methods that I found at this blog:
http://blogs.interknowlogy.com/2012/11/28/dpchangedwinrt/
Which now allow us to do what we want like this:
dataItemsScroller.RegisterDependencyPropertyChanged(() => dataItemsScroller.VerticalOffset, VerticalOffsetChangedHandler);
private void VerticalOffsetChangedHandler(DependencyPropertyChangedEventArgs obj)
{
doctorsOnDutyScroller.ScrollToVerticalOffset(dataItemsScroller.VerticalOffset);
}
I am sure that these issues and a great many more than my small dabbling have not uncovered will be fixed by subsequent releases, for now though these alternatives work quite nicely. Kudos to the original authors of these 2 helpers.
What Does The Demo App Do
As I have stated the attached demo app doesn't do too much right now, as I just wanted to play around a little with WinRT before starting something larger. Shown below is a list of what the demo app does:
- Reads from an in memory repository to fetch simulated appointments
- Shows a read only schedule over time against the simulated in memory appointments
- Allows the user to pan up/down using both Touch/Mouse
- Allows users to click on one of the items on the right at which point a dialog window will be shown
As I say the long term aim is to allow users to create/edit appointments. This was enough to get a quick start with WinRT
How Does It Work
At its heart it is a very simple control, there are 3 ScrollViewer
s:
- One across the top for the time (Hours in my case)
- One down the left hand side representing the resource (Doctors in my case)
- One central one which are the appointments
These are essentially laid out using a standard Grid
control,
where we place things in Row
(s)/Column
(s) and adjust
the ColumnSpan
based on how much time an appointment takes up.
The only clever bit it we hide the ScrollViewer
s ScrollBar
s
from all but the main central area one, and we then have some code behind which
is responsible for not only setting up all the required Grid
(s)/
Row
Column
(s) but also sets up the interactions between
the different ScrollViewer
s. This is shown below:
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)
});
}
}
}
This diagram may help describe this further
CLICK IMAGE FOR LARGER VERSION
The Main Classes
In Memory Repository
This is a simple service that dishes out faux data, that would obvioulsy come from a database in real life
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
No Schedule control would work without some sort of time header, and this
ScheduleControl is no different. Here is my 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; }
}
Which is created in XAML as follows:
<!-- 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
As you can probably tell the DoctorViewModel
is a very simple
ViewModel that is used to represents the left hand side of the ScheduleControl.
Here is the code for the DoctorViewModel, nothing very complicated in here:
[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;
}
}
And here is the main XAML that deals with renedering the DoctorViewModel
, again nothing fancy really
<!-- 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
This is about the only slighltly complicated part of the entire demo
application (well the parts I wrote anyway). This ViewModel is responsible for
creating the HourHeaderViewModel
and DoctorViewModel
,
and would also be responsible for setting up the blank items when I get round to
that bit in the next article.
This ViewModel works in a pretty simple manner, it basically know how big a horizontal time slot is based on the SlotDurationInMins variable, which is defaulted to 15, which means if an appointment is 1 hour long, we would expect full hour header to span 4 columns (60/15 = 4).
Essentiallu with that tiny bit of Maths, we have enough to create all the
HourHeaderViewModel
and DoctorViewModel
objects. Here is the code
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;
}
}
Where we have the following XAML to represent the schedule items.
<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>
Future Work
When I port this code to the fuller StyleMVVM app, I would like to make the schedule control editable by clicking on blank slots, checking for clashes with other appointments etc etc. As such I have left certain bits of code in place that I know I will need, namely these classes:
BlankItemViewModel
: This will represent a blank time slot which
may be clicked, which in time will launch some sort of UI to allow the current
clicked time slot (and possibly more) to be filled with a new appointment. Think
of the way Outlook works. This would also check the next/previously used time
slots and ensure that the new appointment doesn't overlap them.
ScheduleItemTemplateSelector
: As we would effectivly be
rendering either a ScheduleItemViewModel
or a
BlankItemViewModel
, we need some way of selectinh which template to
apply. This ScheduleItemTemplateSelector
code does that. Out of
curiosity here is the ScheduleItemTemplateSelector
code.
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; }
}
That's It
Anyway that's it for now. I guess I bet get busy writing that stuff for Ian, so off I trot. As always if you liked this article, and fancy giving it a vote, comment, that is most welcome.