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

WinRT: StyleMVVM Demo 2 of 2

, 6 Nov 2013 CPOL
Rate this:
Please Sign up or sign in to vote.
StyleMVVM Demo App

Introduction

Series Links

Introduction

This is the second and final part to my StyleMVVM article series. Last time we looked at what comes out of the box with StyleMVVM. This time we will be concentrating on a small demo app that I have made using StyleMVVM.

In this article and its associated code (see link at top the article for code) we will use most of the stuff we previously talked about within the 1st article. So we can expect to cover the following topics:

  • Boostrapping
  • IOC
  • Navigation
  • Validation
  • Suspension
  • Use of core services 

Prerequisites

If you want to try out the code the attached demo code (that is my demo not the one that comes with the StyleMVVM source code), you will need to install the following:

  1. Visual Studio 2012 / 2013
  2. Windows 8 / 8.1
  3. SqlLite Windows 8 stuff : http://sqlite.org/2013/sqlite-winrt80-3080002.vsix (though that is included in the demo code download you just need to run it. It is a Visual Studio Extension so just double click it)
  4. Once you have done step 1 here, ensure that the NuGet packages are all present, and if they aren't restore them. And also ensure that you have references the installed (from step 1) SQLite library 

DemoApp Overview

I wanted to come up with a pretty simple application for my first foray into WinRT and Windows 8 development, but I also wanted something with enough substance to it, that it would not be trivial. I also knew that I didn't really want to write a full server side service layer  (think WCF / REST etc.) to drive the application, as I thought that would detract too much from the client side technology (namely WinRT) that I felt was what I wanted to get across in this article series.

That said I knew that to do anything half decent I would need to store an retrieve state somewhere. My initial thoughts were to use some NoSQL document store such as RavenDB, but alas there did not seem to be a WinRT RavenDB client yet, though I did read stuff around this, and it is coming soon.

So I looked around for some alternatives and settled on a compromise, where it was still SQL based, but not so traditional in that it allows the SQL be generated via the use of certain attributes, and it also has a nice Visual Studio extension to get it to work with WinRT, and also had native async/await support. The DB in question is SQLite, which you can grab the extension for here : http://sqlite.org/2013/sqlite-winrt80-3080002.vsix (though for convenience I have included this in the downloadable application at the top of this article).

Ok so now we know what technologies are involved:

  1. Windows 8
  2. WinRT
  3. SQLLite 

But What Does the Demo App Actually Do?

The demo app is a simple doctors surgery patient/appointment booking system. Here is a breakdown of what it allows for:

  1. Capturing basic patient information (including a web cam captured image)
  2. Allow the creation of new appointments (ALL appointments are in 1/2 hour slots for the demo application)
  3. View ALL appointments for a given day against the current set of doctors that work in the surgery (the set of doctors is static)
  4. Drill into found appointments against a given doctor
  5. View statistics about how many appointments are being scheduled against the current set of doctors for a give day. 

DemoApp Breakdown

In this section we will talk about the various components within the attached demo app. There are some things that are a common concern, such as LayoutAwarePage, which will talk about first, then it will be on to talk about the individual pages in more detail. Though I will not be covering stuff I already covered in the 1st article, as it just feels like too much repetition if I were to do that. 

LayoutAwarePage

One cross cutting concern that I wanted to tackle, was making my app capable of being shown in any of the supported form factors. For me those are the form factors supported by the  Windows8 simulator.

So namely these resolutions as shown in the Simulator:

I also wanted to allow for any orientation, such as Portrait / Landscape / Filled etc. I have tested all of the different sizes/orientations using the Simulator, which really is a valuable tool. I don't think I have missed anything, it should work in any size and any orientation.

So How Does It All Work In Any Size And Any Orientation?

The trick to making a WinRT that will work for any size and any orientation is achieved using 2 things

THING 1: Panels

For the sizing make sure you use the standard Panels, such as Grid, StackPanel, etc. These are built with very good layout algorithms. So use them

THING 2: Adaptive Layout

For the adaptive layout, we use a LayoutAwarePage as provided by StyleMVVM. And what we then need to do, it to use some VisualStates for the various orientations such as:

  1. FullScreenLandscape
  2. Filled
  3. FullScreenPortrait
  4. Snapped
<Common:LayoutAwarePage
    ......
    ......
    ......
    ......>
    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">


        <Grid x:Name="notSnappedGrid" 
          Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
            ....
            ....
        </Grid>

        <Grid x:Name="snappedGrid" Visibility="Collapsed">
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" 
            VerticalAlignment="Center">
                <Image Source="../Assets/Warning.png" Width="40" Height="40" 
            Margin="5" VerticalAlignment="Center"/>
                <TextBlock Text="DEMO DOESN'T ALLOW SNAP MODE" 
            Style="{StaticResource SnapModeLabelStyle}"/>
            </StackPanel>
        </Grid>

        <VisualStateManager.VisualStateGroups>

            <!-- Visual states reflect the application's view state -->
            <VisualStateGroup x:Name="ApplicationViewStates">

                <VisualState x:Name="FullScreenLandscape"/>
                <VisualState x:Name="Filled"/>
                <VisualState x:Name="FullScreenPortrait"/>

                <!-- The back button and title have different styles when snapped -->
                <VisualState x:Name="Snapped">
                    <Storyboard>
                        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="notSnappedGrid" 
                Storyboard.TargetProperty="Visibility">
                            <DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed"/>
                        </ObjectAnimationUsingKeyFrames>
                        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="snappedGrid" 
                Storyboard.TargetProperty="Visibility">
                            <DiscreteObjectKeyFrame KeyTime="0" Value="Visible"/>
                        </ObjectAnimationUsingKeyFrames>
                    </Storyboard>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
    </Grid>
</Common:LayoutAwarePage>

The trick is that certain areas of the UI will be made Visible while others will be made invisible, or different DataTemplate(s) may be applied depending on the orientation. I feel the above LayoutAwarePage covers most of what you need to do.

Now Microsoft had some guidelines somewhere that stated that all apps must support the snapped view. Well sorry this doesn't work for ALL apps. My app for example doesn't really work in snapped mode, so if you try and snap the attached demo app you will get this shown to you, where I supply a snap view essentially saying snap mode is not supported by my demo app.

SQL Lite Data Access Service

One of the cross cutting concerns that the application provides is using a SQLite database, as such it is pretty reasonable that there is some sort of data access service to communicate with the SQLite database. In the demo app this is available as a ISqlLiteDatabaseService implementation where the ISqlLiteDatabaseService interface looks like this:

public interface ISqlLiteDatabaseService
{
    IAsyncOperation<int> SavePatientDetailAsync(PatientDetailModel patient);
    IAsyncOperation<int> SaveScheduleItemAsync(ScheduleItemModel scheduleItem);
    IAsyncOperation<List<DoctorModel>> GetDoctorsAsync { get; }
    IAsyncOperation<PatientDetailModel> GetPatientAsync(int patientId);
    IAsyncOperation<List<PatientDetailModel>> GetPatientsAsync();
    IAsyncOperation<List<ScheduleItemModel>> FetchAppointmentsForDoctorAsync(int doctorId, DateTime date);
    IAsyncOperation<bool> DeleteAppointmentAsync(int scheduleItemId);
    IAsyncOperation<Dictionary<DoctorModel, List<ScheduleItemModel>>> FetchScheduleItemsAsync(DateTime date);
    IAsyncOperation<Dictionary<DoctorModel, List<ScheduleItemModel>>> SearchScheduleItemsAsync(string name);
}

Where the general implementation looks like this (I do not want or need to detail every method, they all follow roughly the same idea):

[Singleton]
[Export(typeof(ISqlLiteDatabaseService))]
public class SqlLiteDatabaseService : ISqlLiteDatabaseService
{
    private string dbRootPath = Windows.Storage.ApplicationData.Current.LocalFolder.Path;
    private Lazy<Task<List<DoctorModel>>> doctorsLazy;
    private List<string> doctorNames = new List<string>();

    public SqlLiteDatabaseService()
    {
        doctorNames.Add("Dr John Smith");
        doctorNames.Add("Dr Mathew Marson");
        doctorNames.Add("Dr Fred Bird");
        doctorNames.Add("Dr Nathan Fills");
        doctorNames.Add("Dr Brad Dens");
        doctorNames.Add("Dr Nathan Drews");
        doctorNames.Add("Dr Frank Hill");
        doctorNames.Add("Dr Lelia Spark");
        doctorNames.Add("Dr Amy Wing");
        doctorNames.Add("Dr Bes Butler");
        doctorNames.Add("Dr John Best");
        doctorNames.Add("Dr Philip Mungbean");
        doctorNames.Add("Dr Jude Fink");
        doctorNames.Add("Dr Petra Nicestock");
        doctorNames.Add("Dr Ras Guul");


        doctorsLazy = new Lazy<Task<List<DoctorModel>>>(async delegate 
        {
            List<DoctorModel> doctors = await GetDoctorsInternal();
            return doctors;
        });
    }

    public IAsyncOperation<List<DoctorModel>> GetDoctorsAsync 
    { 
        get
        {
            return doctorsLazy.Value.AsAsyncOperation<List<DoctorModel>>();
        }
    }

    private async Task<List<DoctorModel>> GetDoctorsInternal()
    {
        var db = new SQLiteAsyncConnection(
            Path.Combine(dbRootPath, "StyleMVVM_SurgeryDemo.sqlite"));
        var cta = await db.CreateTableAsync<DoctorModel>();

        var query = db.Table<DoctorModel>();
        var doctors = await query.ToListAsync();
        if (doctors.Count == 0)
        {
            foreach (var doctorName in doctorNames)
            {
                var id = await db.InsertAsync(new DoctorModel() { Name = doctorName });
            }
        }
        query = db.Table<DoctorModel>();
        doctors = await query.ToListAsync();
        return doctors;
    }

    .....
    .....
    .....
    .....
}

Because SQLite has a nice .NET 4.5 API we are quite free to use Async / Await such as this:

Doctors = await sqlLiteDatabaseService.GetDoctorsAsync;

Main Page

The main page is little more than a collection of buttons which each allow a page to be loaded. I guess one thing that is worth pointing out here (since its our 1st actual meeting with the IOC container (so far)) is how we can import stuff from the container.

Here is the constructor from the MainPageViewModel:

[ImportConstructor]
public MainPageViewModel(IEnumerable<IPageInfo> pageInfos)
{
    Pages = new List<IPageInfo>(pageInfos);
    Pages.Sort((x, y) => x.Index.CompareTo(y.Index));
}

See how we use the StyleMVVM ImportConstructorAttribute here to import some pages

Click the image for a larger version

We can then navigate to each of the pages using some code like this:

public void ItemClick(ItemClickEventArgs args)
{
    IPageInfo page = args.ClickedItem as IPageInfo;

    if (page != null)
    {
        Navigation.Navigate(page.ViewName);
    }
}

Which we trigger from the XAML as follows

<Grid x:Name="notSnappedGrid" Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
    <GridView ItemsSource="{Binding Pages}"
            Height="210"
            HorizontalAlignment="Center" VerticalAlignment="Center"
            SelectionMode="None" IsItemClickEnabled="True" 
            View:EventHandlers.Attach="ItemClick => ItemClick($eventArgs)">
    .....
    .....
    .....
</Grid>

Patient Details Page

This page allow the entry of new patient details which will have the following validation performed on them:

[Export(typeof(IFluentRuleProvider<PatientDetailsPageViewModel>))]
public class FluentValidationForPatientDetailsVM : IFluentRuleProvider<PatientDetailsPageViewModel>
{
    public void ProvideRules(IFluentRuleCollection<PatientDetailsPageViewModel> collection)
    {
        collection.AddRule("PrefixRule")
                    .Property(x => x.Prefix)
                    .IsRequired()
                    .When.Property(x => x.EmailAddress)
                    .IsNotEmpty();
    }
}

[Export(typeof(IValidationMethodProvider<PatientDetailsPageViewModel>))]
public class MethodValidationForPatientDetailsVM : 
       IValidationMethodProvider<PatientDetailsPageViewModel>
{
    public void ProvideRules(IValidationMethodCollection<PatientDetailsPageViewModel> methodCollection)
    {
        methodCollection.AddRule(MinLengthRules).
            MonitorProperty(x => x.FirstName).MonitorProperty(x => x.LastName);
    }

    private void MinLengthRules(IRuleExecutionContext<PatientDetailsPageViewModel> obj)
    {
        if(!string.IsNullOrEmpty(obj.ValidationObject.FirstName))
        {
            if(obj.ValidationObject.FirstName.Length < 3)
            {
                obj.AddError(x => x.FirstName, 
                  "Your first name can't be less than 3 characters long");
                obj.Message = "The data entered is invalid";
            }
        }

        if(!string.IsNullOrEmpty(obj.ValidationObject.LastName))
        {
            if(obj.ValidationObject.FirstName.Length < 3)
            {
                obj.AddError(x => x.FirstName, "Your last name can't be less than 3 characters long");
                obj.Message = "The data entered is invalid";
            }
        }

        if (!string.IsNullOrEmpty(obj.ValidationObject.ImageFilePath))
        {
            obj.AddError(x => x.FirstName, "You must supply an image");
            obj.Message = "The data entered is invalid";
        }
    }
}

Click the image for a larger version

It is also possible to capture a web cam image that would be stored against the hypothetical patient whose details we are capturing. Obviously as we are dealing with a WebCam we need to write some code to capture an image from it. Here is the WebCam code:

The WebCam code is initiated when we click the "Capture Image" button.

[Singleton]
[Export(typeof(IWebCamService))]
public class WebCamService : IWebCamService
{
    public IAsyncOperation<string> CaptureImageAsync()
    {
        return CaptureImageInternal().AsAsyncOperation();
    }

    private async Task<string> CaptureImageInternal()
    {
        var captureHelper = new Windows.Media.Capture.CameraCaptureUI();
        captureHelper.PhotoSettings.CroppedAspectRatio = new Size(4, 3);
        captureHelper.PhotoSettings.MaxResolution = CameraCaptureUIMaxPhotoResolution.Large3M;
        captureHelper.PhotoSettings.Format = CameraCaptureUIPhotoFormat.Png;
        IStorageFile file = await captureHelper.CaptureFileAsync(CameraCaptureUIMode.Photo);
        if (file != null)
        {
            return file.Path;
        }
        else
        {
            return null;
        }
    }
}

As with all thing WinRT we need to make sure that this using Async/Await. When we click the "Capture Image" button we will be presented with a screenshot similar to that shown below, where we can press OK using touch or the mouse.

Click the image for a larger version

Once the image has been captured we can choose to crop it using the standard cropping tool.

Click the image for a larger version

And when we are finally happy and have cropped our image it will appear as part of the patient details we are capturing, which should look roughly like the following:

Click the image for a larger version

Here is the most relevant parts of the ViewModel for this page:

[Syncable]
public class PatientDetailsPageViewModel : PageViewModel, IValidationStateChangedHandler
{
    ......
    ......
    ......
    ......

    [ImportConstructor]
    public PatientDetailsPageViewModel(
        ISqlLiteDatabaseService sqlLiteDatabaseService,
        IMessageBoxService messageBoxService,
        IWebCamService webCamService)
    {
        this.sqlLiteDatabaseService = sqlLiteDatabaseService;
        this.messageBoxService = messageBoxService;
        this.webCamService = webCamService;

        SaveCommand = new DelegateCommand(ExecuteSaveCommand,
                    x => ValidationContext.State == ValidationState.Valid);
        CaptureImageCommand = new DelegateCommand(ExecuteCaptureImageCommand,
                    x => ValidationContext.State == ValidationState.Valid);
    }

    [Sync]
    public string Prefix
    {
        get { return prefix; }
        set { SetProperty(ref prefix, value); }
    }

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


    [Import]
    public IValidationContext ValidationContext { get; set; }

    [ActivationComplete]
    public void Activated()
    {
        ValidationContext.RegisterValidationStateChangedHandler(this);
    }

    private async void ExecuteSaveCommand(object parameter)
    {
        try
        {
            PatientDetailModel patient = new PatientDetailModel();
            patient.Prefix = this.Prefix;
            patient.FirstName = this.FirstName;
            patient.LastName = this.LastName;
            patient.MiddleName = this.MiddleName;
            patient.EmailAddress = this.EmailAddress;
            patient.ImageFilePath = this.ImageFilePath;
            patient.Detail = patient.ToString();
            int id = await sqlLiteDatabaseService.SavePatientDetailAsync(patient);

            if (id > 0)
            {
                string msg = string.Format("Patient {0} {1} {2}, saved with Id : {3}", 
                             Prefix, FirstName, LastName, id);
                this.Prefix = string.Empty;
                this.FirstName = string.Empty;
                this.LastName = string.Empty;
                this.MiddleName = string.Empty;
                this.EmailAddress = string.Empty;
                this.ImageFilePath = string.Empty;
                this.HasImage = false;
                SaveCommand.RaiseCanExecuteChanged();
                CaptureImageCommand.RaiseCanExecuteChanged();
                await messageBoxService.Show(msg); 
            }
        }
        catch (Exception ex)
        {
            if (ex is InvalidOperationException)
            {
                messageBoxService.Show(ex.Message);
            }
            else
            {
                messageBoxService.Show("There was a problem save the Patient data");
            }
        }
    }

    private IStorageFile tempFile;

    private async void ExecuteCaptureImageCommand(object parameter)
    {
        try
        {
            ImageFilePath = await webCamService.CaptureImageAsync();
            HasImage = !string.IsNullOrEmpty(ImageFilePath);
        }
        catch (Exception ex)
        {
            messageBoxService.Show("There was a problem capturing the image");
        }
    }

    public void StateChanged(IValidationContext context, ValidationState validationState)
    {
        SaveCommand.RaiseCanExecuteChanged();
        CaptureImageCommand.RaiseCanExecuteChanged();
    }
}

Create Appointment Page

A lot of the way this works is pretty standard ViewModel stuff. But lets go through what each of the screen shots does and we then we can look at the ViewModel that drives this page in a bit more detail.

So when we 1st load this page, and assuming you have no appointments yet, and that you have selected a Doctor and a Patient from the ComboBoxes provided you should see something like the screen shot below. From here you can :

  • Choose a date to add appointments for
  • Click on one of the timeslots on the left, which will initiate the adding of a new appointment based on your other selected criteria

Click the image for a larger version

Once you have clicked on one of the timeslots you will see an area where you can enter a message and pick a body position (Front / Side / Back). When you have picked a position you will see an image that matches the sex of the patient and also matches the body position requested.

You will also see a small blue and white circle. This is freely moveable, and can be dragged around the image of the current body. The idea being that this could be used to indicate where the patients problem area is exactly. This point will be used later when it comes time to view the saved appointments.

Click the image for a larger version

Once an appointment has been saved it will be shown in the time slot list on the left hand side of the screen, and several of the data entry fields, and patient ComboBox will be cleared, as we would like those to be selected again for any new appointment that gets created, as it would more than likely be for a different patient/problem and body position.

There is also the ability to view and delete a selected appointment.

Click the image for a larger version

I think one of the more interesting things in this page is the way you drag the circle around the currently view body. This is easily achieved using a custom control called InteractiveBodyControl, which contains a Canvas and a styled Button . The circle is effectively the Style for the Button.

<Canvas Margin="10" x:Name="canv">
    <Grid HorizontalAlignment="Center" 
            VerticalAlignment="Center">
        <Image x:Name="imgMaleFront" Source="../Assets/MaleFront.png" Opacity="0"/>
        <Image x:Name="imgMaleSide" Source="../Assets/MaleSide.png" Opacity="0"/>
        <Image x:Name="imgMaleBack" Source="../Assets/MaleBack.png" Opacity="0"/>
        <Image x:Name="imgFemaleFront" Source="../Assets/FemaleFront.png" Opacity="0"/>
        <Image x:Name="imgFemaleSide" Source="../Assets/FemaleSide.png" Opacity="0"/>
        <Image x:Name="imgFemaleBack" Source="../Assets/FemaleBack.png" Opacity="0"/>
    </Grid>
    <Button ManipulationMode="All" Loaded="Button_Loaded"
            ManipulationDelta="Button_ManipulationDelta"
            Width="40" Height="40">
        <Button.Template>
            <ControlTemplate>
                <Grid>
                    <Ellipse Width="40" Height="40" Fill="#ff4617B4" 
                            HorizontalAlignment="Center" VerticalAlignment="Center"/>
                    <Ellipse Width="20" Height="20" Fill="White"
                            HorizontalAlignment="Center" VerticalAlignment="Center"/>
                </Grid>
            </ControlTemplate>
        </Button.Template>
    </Button>
</Canvas>

Obviously only one of the body images is shown at once (more on this later). We also have this code that deals with moving the circle around, where we just use the standard ManipulationDelta event as follows:

public Point BodyPoint
{
    get
    {
        return new Point(Canvas.GetLeft(button), Canvas.GetTop(button));
    }
    set
    {
        Canvas.SetLeft(button, value.X);
        Canvas.SetTop(button, value.Y);
    }
}

private void Button_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
{
    Button button = (Button)sender;

    var maxWidth = this.Width;
    var maxHeight = this.Height;

    double newX = Canvas.GetLeft(button) + e.Delta.Translation.X;
    double newY = Canvas.GetTop(button) + e.Delta.Translation.Y;

    newX = Math.Max(0, newX);
    newX = Math.Min(maxWidth, newX);

    newY = Math.Max(0, newY);
    newY = Math.Min(maxHeight, newY);

    Canvas.SetLeft(button, newX);
    Canvas.SetTop(button, newY);
}

Also of note is the fact that we can set a Point (for when we are viewing an existing appointment), and get the current Point such that is can be saved against the current appointment.

The other interesting bit is how the body gets swapped out based on the sex of the patient, and the requested body position. As we just saw above there are actually six images:

  • MaleFront.png
  • MaleSide.png
  • MaleBack.png
  • FemaleFront.png
  • FemaleSide.png
  • FemaleBack.png

So what we need to do is essentially hide all of them except the one that matches the sex of the patient and also the requested body position. To do this, we know we need to use VisualStates, but how do we get our ViewModel to drive a new VisualState selection on the View?

So it turns out this is easily achievable in StyleMVVM thanks to the PageViewModel base class that I talked about in the last article. This class gives us access to the View from our ViewModel (purists that think this is wrong, ba humbug). So here is the code:

 [Syncable]
public class CreateAppointmentsPageViewModel : PageViewModel, IValidationStateChangedHandler
{
    .....
    .....
    .....
    .....
    private void ChangeBodyBasedOnPatientAndBodyType()
    {

        if (CurrentEditMode == EditMode.CreateNew || CurrentEditMode == EditMode.ViewExisting)
        {
            if (Patient == null || Patients == null || BodyViewType == null)
                return;

            if (Patient.Prefix == "Mr")
            {
                (this.View as ICreateAppointmentsPage).GoToVisualState(
                       string.Format("Male{0}State", BodyViewType.Value));
            }
            if (Patient.Prefix == "Mrs")
            {
                (this.View as ICreateAppointmentsPage).GoToVisualState(
                       string.Format("Female{0}State", BodyViewType.Value));
            }

            ShowImage = true;

        }
        else
        {
            showImage = false;
        }
    }
    .....
    .....
    .....
    .....
}

Where the important part is this (this.View as ICreateAppointmentsPage).GoToVisualState(string.Format("Female{0}State", BodyViewType.Value)); . So let's now have a look at this ViewModels view.

public interface ICreateAppointmentsPage
{
    void GoToVisualState(string visualState);
    Point BodyPoint { get; set; }
}


[Export]
public sealed partial class CreateAppointmentsPage : LayoutAwarePage, ICreateAppointmentsPage
{
    InteractiveBodyControl bodyControl = null;
    private string currentBodyState;

    public CreateAppointmentsPage()
    {
        this.InitializeComponent();
        var appSearchPane = SearchPane.GetForCurrentView();
        appSearchPane.PlaceholderText = "Name or a date (dd/mm/yy)";
        this.LayoutUpdated += CreateAppointmentsPage_LayoutUpdated;
    }

    void CreateAppointmentsPage_LayoutUpdated(object sender, object e)
    {
        GoToCurrentState();
    }

    public void GoToVisualState(string visualState)
    {
        currentBodyState = visualState;
        GoToCurrentState();
    }

    private void BodyControl_Loaded(object sender, RoutedEventArgs e)
    {
        bodyControl = sender as InteractiveBodyControl;
    }

    private void GoToCurrentState()
    {
        if (currentBodyState == null || bodyControl == null)
            return;

        bodyControl.GoToVisualState(currentBodyState);
    }

    public Point BodyPoint
    {
        get
        {
            return this.bodyControl.BodyPoint;
        }
        set
        {
            this.bodyControl.BodyPoint = value;
        }
    }
}

It can be seen that the View contains code to tell the contained InteractiveBodyControl to go to a particular state. It also contains code which will get/set the Point used by the InteractiveBodyControl.

This is a powerful technique that can be used to do quite view centric stuff from a ViewModel. PageViewModel is awesome and one of my favourite parts of StyleMVVM

The rest of the way this ViewModel works is all pretty standard stuff, so I will not waste time discussing this.

View Appointments Page

This page is largely the same code that I have already published in a previous article : http://www.codeproject.com/Articles/654374/WinRT-Simple-ScheduleControl

Essentially its a schedule control, that allows the user to scroll through the appointments using Touch/Mouse. It is a pretty complex arrangement, and for this particular part it may be better to read the full article as it has a few moving parts.

Click the image for a larger version

View Specific Doctors Appointments Page

This page simply allows you to view specific appointments that were previously saved against a Doctor. Most of the data is static, the only user interaction is to click on the White circle which will show you the message that was recorded whilst creating the original appointment. Most of this is bulk standard MVVM code, so I think for this page, there is no need to go into more detail.

Click the image for a larger version

View Statistics

On this page you can view statistics, about how many appointments are currently stored and which Doctor they are stored against for a given day.

Click the image for a larger version

For this charting I am using the WinRT XAML ToolKit - Data Visualization Controls (don't worry the attached demo app includes the correct NuGet package already).The WinRT XAML ToolKit includes a PieChart, which is what I am using.

Here is the XAML that creates the PieChart.

<charting:Chart
    x:Name="PieChart"
    Title="Doctors appointments for selected Date"
    Margin="0,0">
    <charting:Chart.Series>
        <charting:PieSeries 
            IndependentValueBinding="{Binding Name}"
            DependentValueBinding="{Binding Value}"
            IsSelectionEnabled="True" />
    </charting:Chart.Series>
    <charting:Chart.LegendStyle>
        <Style TargetType="datavis:Legend">
            <Setter Property="VerticalAlignment" Value="Stretch" />
            <Setter Property="Background" Value="#444" />
            <Setter Property="ItemsPanel">
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <controls:UniformGrid Columns="1" Rows="5" />
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="TitleStyle">
                <Setter.Value>
                    <Style TargetType="datavis:Title">
                        <Setter  Property="Margin" Value="0,5,0,10" />
                        <Setter Property="FontWeight" Value="Bold" />
                        <Setter Property="HorizontalAlignment" Value="Center" />
                    </Style>
                </Setter.Value>
            </Setter>
            <Setter Property="ItemContainerStyle">
                <Setter.Value>
                    <Style TargetType="charting:LegendItem">
                        <Setter Property="Template">
                            <Setter.Value>
                                <ControlTemplate TargetType="charting:LegendItem">
                                    <Border
                                            MinWidth="200"
                                            Margin="20,10"
                                            CornerRadius="0"
                                            VerticalAlignment="Stretch"
                                            HorizontalAlignment="Stretch"
                                            Background="{Binding Background}">
                                        <datavis:Title
                                            HorizontalAlignment="Center"
                                            VerticalAlignment="Center"
                                            FontSize="24"
                                            FontWeight="Normal"
                                            FontFamily="Segeo UI"
                                            Content="{TemplateBinding Content}" />
                                    </Border>
                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                    </Style>
                </Setter.Value>
            </Setter>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="datavis:Legend">
                        <Border
                                Background="{TemplateBinding Background}"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}"
                                Padding="2">
                            <Grid>
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="Auto" />
                                    <RowDefinition />
                                </Grid.RowDefinitions>
                                <datavis:Title
                                    Grid.Row="0"
                                    x:Name="HeaderContent"
                                    Content="{TemplateBinding Header}"
                                    ContentTemplate="{TemplateBinding HeaderTemplate}"
                                    Style="{TemplateBinding TitleStyle}" />
                                <ScrollViewer
                                    Grid.Row="1"
                                    VerticalScrollBarVisibility="Auto"
                                    BorderThickness="0"
                                    Padding="0"
                                    IsTabStop="False">
                                    <ItemsPresenter
                                        x:Name="Items"
                                        Margin="10,0,10,10" />
                                </ScrollViewer>
                            </Grid>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </charting:Chart.LegendStyle>
</charting:Chart>

Where the series data is bound to a IEnumerable<NameValueItemViewModel>, where a NameValueItemViewModel looks like this

public class NameValueItemViewModel
{
    public string Name { get; set; }
    public int Value { get; set; }
}

Unfortunately I did not seem to be able to bind the series data directly in the XAML. So I had to resort to the same trick as before (with the VisualState being set from the ViewModel, where we use the PageViewModel and an interface on the View).

[Export]
public sealed partial class ViewStatsPage : LayoutAwarePage, IViewStatsPage
{
    public void SetChart(List>NameValueItemViewModel> items)
    {
        ((PieSeries)this.PieChart.Series[0]).ItemsSource = items;
    }
}

Interacting With The SharePane

One of the other things I really wanted to get to work, was interacting with SearchPane. I did manage to achieve what I wanted to do. But my god the way that WinRT currently expects you to interact with the SearchPane is pretty dire in my opinion. It literally forces you to slam more and more code into the App.xaml.cs or delegate that off to some other place. I don't know how I would like this to work but I do know that the way it is right now is plain dirty. I fine it course and vulgar. 

Click the image for a larger version

It all starts out with some dirty misplaced code in App.xaml.cs which is as follows:

private void ConfigureSearchContract()
{
    var appSearchPane = SearchPane.GetForCurrentView();
    appSearchPane.PlaceholderText = "Name or a date (dd/mm/yy)";
}

// <summary>
/// Invoked when the application is activated to display search results.
/// </summary>
/// <param name="args">Details about the activation request.</param>
protected async override void OnSearchActivated(
  Windows.ApplicationModel.Activation.SearchActivatedEventArgs args)
{
    ConfigureSearchContract();

    if (args.PreviousExecutionState != ApplicationExecutionState.Running)
    {
        LaunchBootStrapper();
    }

    // If the Window isn't already using Frame navigation, insert our own Frame
    var previousContent = Window.Current.Content;
    var frame = previousContent as Frame;

    // If the app does not contain a top-level frame, it is possible that this 
    // is the initial launch of the app. Typically this method and OnLaunched 
    // in App.xaml.cs can call a common method.
    if (frame == null)
    {
        // Create a Frame to act as the navigation context and associate it with
        // a SuspensionManager key
        frame = new Frame();
        SuspensionManager.RegisterFrame(frame, "AppFrame");

        if (args.PreviousExecutionState == ApplicationExecutionState.Terminated)
        {
            // Restore the saved session state only when appropriate
            try
            {
                await SuspensionManager.RestoreAsync();
            }
            catch (Exception)
            {
                //Something went wrong restoring state.
                //Assume there is no state and continue
            }
        }
    }

    frame.Navigate(typeof(SearchResultsView), args.QueryText);
    Window.Current.Content = frame;

    // Ensure the current window is active
    Window.Current.Activate();
}

Where each full page view also has this in it's code behind, which is responsible for showing the watermark text in the search TextBox in the SearchPane

var appSearchPane = SearchPane.GetForCurrentView();
appSearchPane.PlaceholderText = "Name or a date (dd/mm/yy)";

Click the image for a larger version

What happens is that when the user types a value into the TextBox in the SearchPane, the SearchResultView is loaded, and the current search value is passed in the the Navigation parameters, which are then available from the PageViewModel.OnNavigatedTo(..) method, and from there we can run the search. This may become clearer, if I show you the SearchResultViewModel, which is shown below.

[Singleton]
[Syncable]
public class SearchResultsViewModel : PageViewModel
{
    private string searchString;
    private List<SearchResultViewModel> results;
    private bool hasResults = false;

    private ISqlLiteDatabaseService sqlLiteDatabaseService;
        
    [ImportConstructor]
    public SearchResultsViewModel(
        ISqlLiteDatabaseService sqlLiteDatabaseService)
    {
        this.sqlLiteDatabaseService = sqlLiteDatabaseService;
    }

    [Sync]
    public string SearchString
    {
        get { return searchString; }
        private set { SetProperty(ref searchString, value); }
    }

    [Sync]
    public List<SearchResultViewModel> Results
    {
        get { return results; }
        private set { SetProperty(ref results, value); }
    }

    [Sync]
    public bool HasResults
    {
        get { return hasResults; }
        private set { SetProperty(ref hasResults, value); }
    }

    private async Task<SearchResultViewModel> CreateScheduleItemViewModel(ScheduleItemModel model)
    {
        var doctors = await sqlLiteDatabaseService.GetDoctorsAsync;
        var doctorName = doctors.Single(x => x.DoctorId == model.DoctorId).Name;

        var patient = await sqlLiteDatabaseService.GetPatientAsync(model.PatientId);

        return new SearchResultViewModel(model.Date, patient.FullName, 
            new Time(model.StartTimeHour, model.StartTimeMinute),
            new Time(model.EndTimeHour, model.EndTimeMinute), doctorName);

    }

    protected async override void OnNavigatedTo(object sender, StyleNavigationEventArgs e)
    {
        SearchString = NavigationParameter as string;
        DoSearch(SearchString);
    }


    private async void DoSearch(string searchString)
    {
        try
        {
            DateTime date;
            Dictionary<DoctorModel, List<ScheduleItemModel>> 
              appointments = new Dictionary<DoctorModel, List<ScheduleItemModel>>();
            if (DateTime.TryParse(SearchString, out date))
            {
                appointments = await sqlLiteDatabaseService.FetchScheduleItemsAsync(date);
            }
            else
            {
                appointments = await sqlLiteDatabaseService.SearchScheduleItemsAsync(searchString);
            }

            if (appointments.Any())
            {
                CreateAppointments(appointments);
            }
            else
            {
                HasResults = false;
            }
        }
        catch
        {
            //not much we can do about it, other than set a property.
            //Would not be good form to show a messagebox using Search contract
            Results = new List<SearchResultViewModel>();
            HasResults = false;
        }
    }

    protected async void CreateAppointments(Dictionary<DoctorModel, 
                         List<ScheduleItemModel>> appointments)
    {
        List<SearchResultViewModel> localResults = new List<SearchResultViewModel>();
        foreach (KeyValuePair<DoctorModel, List<ScheduleItemModel>> appointment in appointments)
        {
            if (appointment.Value.Any())
            {
                foreach (var scheduleItemModel in appointment.Value)
                {
                    var resulVm = await CreateScheduleItemViewModel(scheduleItemModel);
                    localResults.Add(resulVm);
                }
            }
        }

        if (localResults.Any())
        {
            Results = localResults;
            HasResults = true;
        }
        else
        {
            Results = new List<SearchResultViewModel>();
            HasResults = false;
        }

    }

}

Once we have the results it is just a matter of binding them to some control, which is done as follows, where we simply use a standard GridView control with a custom DataTemplate

<GridView
    Visibility="{Binding HasResults, 
      Converter={StaticResource BoolToVisibilityConv}, ConverterParameter='False'}"
    x:Name="resultsGridView"
    AutomationProperties.AutomationId="ResultsGridView"
    AutomationProperties.Name="Search Results"
    Margin="20"
    TabIndex="1"
    Grid.Row="0"
    SelectionMode="None"
    IsSwipeEnabled="false"
    IsItemClickEnabled="False"
    ItemsSource="{Binding Source={StaticResource resultsViewSource}}"
    ItemTemplate="{StaticResource ResultsDataTemplate}">

    <GridView.ItemContainerStyle>
        <Style TargetType="Control">
            <Setter Property="Margin" Value="20"/>
        </Style>
    </GridView.ItemContainerStyle>
</GridView>

<DataTemplate x:Key="ResultsDataTemplate">
    <Grid HorizontalAlignment="Left" Width="Auto" 
              Height="Auto" Background="#ff4617B4">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <Image Grid.Column="0" VerticalAlignment="Center"
                Width="50" Height="50" Margin="10"
                Source="{Binding Image}"/>

        <StackPanel Grid.Column="1" Orientation="Vertical" Margin="10">
            <StackPanel Orientation="Horizontal">
                <TextBlock Foreground="CornflowerBlue"
                    FontSize="24"
                    FontFamily="Segeo UI"
                    FontWeight="Normal"
                    Text="{Binding Date}" />
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="Doctor :" Foreground="White" 
                   Style="{StaticResource SearchLabelStyle}"/>
                <TextBlock Text="{Binding DoctorName}" 
                   Foreground="White" Style="{StaticResource SearchLabelStyle}"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding StartTime}" 
                  Style="{StaticResource SearchLabelStyle}" Foreground="White" />
                <TextBlock Text="-" Foreground="White" 
                  Style="{StaticResource SearchLabelStyle}"/>
                <TextBlock Text="{Binding EndTime}" 
                  Foreground="White" Style="{StaticResource SearchLabelStyle}"/>
                <TextBlock Text=" " Foreground="White" 
                  Style="{StaticResource SearchLabelStyle}"/>
                <TextBlock Text="{Binding PatientName}" 
                  Foreground="White" Style="{StaticResource SearchLabelStyle}"/>
            </StackPanel>
        </StackPanel>
    </Grid>
</DataTemplate>

That's It

OK so that was a little walk through of the StyleMVVM demo app, and my1st (an I have to say probably last) Windows 8 full application. I did not really enjoy the overall experience to be honest, I think WinRT needs a lot of work done on it to make anywhere near useful/productive. Right now it just feels very cobbled together and hacky, and loads of past lessons in both WPF and Silverlight seem to have been lost somewhere along the way, which is a crying shame.

I am also going to be steering clear of any UI work for a while and getting stuck into learning a new language, which will F#. I think I will probably write some blog posts on my journey (literally from nothing) into F#.

As always any votes/comments are welcome, and I am sure Ian would appreciate people taking his baby (StyleMVVM of course) for a spin.

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

 
QuestionJust one problem Pinmembertomlev4-Nov-13 5:57 
AnswerRe: Just one problem PinmvpSacha Barber4-Nov-13 6:02 
QuestionTres impressive sir PinprotectorPete O'Hanlon4-Nov-13 5:50 
AnswerRe: Tres impressive sir PinmvpSacha Barber4-Nov-13 6:00 
GeneralMy vote of 5 PinmvpSacha Barber4-Nov-13 4:26 
GeneralRe: My vote of 5 Pinmembertoantvo4-Nov-13 17:15 
GeneralRe: My vote of 5 PinmvpSacha Barber4-Nov-13 19:38 

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 | Terms of Use | Mobile
Web02 | 2.8.141223.1 | Last Updated 6 Nov 2013
Article Copyright 2013 by Sacha Barber
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid