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:
="1.0"="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
{
public App()
{
this.InitializeComponent();
this.Suspending += OnSuspending;
}
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
Frame rootFrame = Window.Current.Content as Frame;
if (rootFrame == null)
{
rootFrame = new Frame();
if (args.PreviousExecutionState == ApplicationExecutionState.Terminated)
{
}
Window.Current.Content = rootFrame;
}
if (rootFrame.Content == null)
{
if (!rootFrame.Navigate(typeof(MainPage), args.Arguments))
{
throw new Exception("Failed to create initial page");
}
}
Window.Current.Activate();
}
private void OnSuspending(object sender, SuspendingEventArgs e)
{
var deferral = e.SuspendingOperation.GetDeferral();
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 windows Frame
.
Which in this case in MainPage
(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 :
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
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:
<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 <code>
Row(s)/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()
{
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:
<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
<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)
{
}
public void RemoveScheduleItem(ScheduleItemViewModel scheduleItem)
{
}
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()
{
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>
<ScrollViewer x:Name="hoursScroller" ZoomMode="Disabled" Grid.Row="0" Grid.Column="1"
IsEnabled="False"
VerticalScrollBarVisibility="Hidden" HorizontalScrollBarVisibility="Hidden" >
<ListBox.../>
</ScrollViewer>
<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.