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:
- Visual Studio 2012 / 2013
- Windows 8 / 8.1
- 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)
- 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:
- Windows 8
- WinRT
- 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:
- Capturing basic patient information (including a web cam captured image)
- Allow the creation of new appointments (ALL appointments are in 1/2 hour
slots for the demo application)
- View ALL appointments for a given day against the current set of doctors
that work in the surgery (the set of doctors is static)
- Drill into found appointments against a given doctor
- 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:
FullScreenLandscape
Filled
FullScreenPortrait
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>
<VisualStateGroup x:Name="ApplicationViewStates">
<VisualState x:Name="FullScreenLandscape"/>
<VisualState x:Name="Filled"/>
<VisualState x:Name="FullScreenPortrait"/>
<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)";
}
protected async override void OnSearchActivated(
Windows.ApplicationModel.Activation.SearchActivatedEventArgs args)
{
ConfigureSearchContract();
if (args.PreviousExecutionState != ApplicationExecutionState.Running)
{
LaunchBootStrapper();
}
var previousContent = Window.Current.Content;
var frame = previousContent as Frame;
if (frame == null)
{
frame = new Frame();
SuspensionManager.RegisterFrame(frame, "AppFrame");
if (args.PreviousExecutionState == ApplicationExecutionState.Terminated)
{
try
{
await SuspensionManager.RestoreAsync();
}
catch (Exception)
{
}
}
}
frame.Navigate(typeof(SearchResultsView), args.QueryText);
Window.Current.Content = frame;
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
{
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.