Click here to Skip to main content
Click here to Skip to main content

WinRT : Simple ScheduleControl

, 16 Sep 2013 CPOL
Rate this:
Please Sign up or sign in to vote.

 

       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 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:

<!-- 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 ScrollViewers:

  • 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 ScrollViewers ScrollBars 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 Row(s)/Column(s) but also sets up the interactions between the different ScrollViewers. 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Sacha Barber
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)
 
- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence
 
Both of these at Sussex University UK.
 
Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
QuestionWow, you did it again! PinmemberSuperJMN-CandyBeat13-Feb-14 4:27 
AnswerRe: Wow, you did it again! PinmvpSacha Barber13-Feb-14 5:09 
QuestionI thought I felt the earth tilt on its axis. PinprotectorPete O'Hanlon20-Sep-13 1:23 
AnswerRe: I thought I felt the earth tilt on its axis. PinmvpSacha Barber22-Sep-13 19:53 
GeneralNice. Pinprofessionaldb7uk18-Sep-13 13:01 
GeneralRe: Nice. [modified] PinmvpSacha Barber18-Sep-13 19:18 
GeneralRe: Nice. Pinprofessionaldb7uk23-Sep-13 0:11 
GeneralRe: Nice. PinmvpSacha Barber23-Sep-13 2:47 
GeneralMy vote of 5 PinprofessionalPrasad Khandekar16-Sep-13 23:47 
GeneralRe: My vote of 5 PinmvpSacha Barber17-Sep-13 0:35 
GeneralMy vote of 5 PinmemberVolynsky Alex16-Sep-13 14:51 
GeneralRe: My vote of 5 PinmvpSacha Barber16-Sep-13 21:44 
GeneralRe: My vote of 5 PinmemberVolynsky Alex17-Sep-13 8:30 
QuestionMy vote of 5 PinprofessionalIan P Johnson16-Sep-13 11:04 
AnswerRe: My vote of 5 PinmvpSacha Barber16-Sep-13 21:44 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.141015.1 | Last Updated 16 Sep 2013
Article Copyright 2013 by Sacha Barber
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid