Uutiset

Adafyn kuulumisia

Building Widget Dashboard using UWP and Caliburn.Micro

<p><a href="http://mikaelkoskinen.net/posts/files/1b271f2f-09af-486d-af2b-28a1efb67562.png"><img title="image" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; float: right; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="image" src="http://mikaelkoskinen.net/posts/files/e96904b9-3c10-499e-b300-369874c7067a.png" width="429" align="right" height="307"></a>In this tutorial we will create an UWP Dashboard app. The dashboard will contain widgets where each widget is a self contained part, providing functionality to the app. We will be using Caliburn.Micro to build the app.</p> <h2>Background</h2> <p>We all have used widget based dashboard apps. VSTS’ project front page is just one example (you can see red arrows pointing to individual widgets):</p> <p><a href="http://mikaelkoskinen.net/posts/files/bba9b2cb-d5b8-48a2-a3cf-3ffce61f9988.png"><img title="image" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="image" src="http://mikaelkoskinen.net/posts/files/34ad2dd8-33b9-49a5-949c-f71861a2b430.png" width="518" height="310"></a></p> <p>Azure Portal is another good example:</p> <p><a href="http://mikaelkoskinen.net/posts/files/0fac59f6-5b5e-4a7f-abb5-b6d91560d0e7.png"><img title="image" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="image" src="http://mikaelkoskinen.net/posts/files/fdeb8397-b8eb-4448-82e9-477b04ebbf84.png" width="521" height="377"></a></p> <p>Common for widget based dashboards is that <strong>each widget is a self contained part</strong>: It can work alone or with 1000 other widgets without interfering with the other widgets. Another common feature for widget based apps is the <strong>shell</strong>: Shell contains the common application UI (like the toolbar) and hosts the widgets.</p> <h2>Building widget based dashboard using UWP</h2> <p>We’re going to use couple libraries to build the dashboard: <a href="https://caliburnmicro.com/" target="_blank">Caliburn.Micro</a> is used to bring down the amount of code and <a href="https://github.com/telerik/UI-For-UWP" target="_blank">Telerik’s UI for UWP</a> is used for some pretty widgets. The Telerik’s UI components are great but in this case they are completely optional: You can create your widgets and the shell using any controls you want. But I would recommend using Caliburn.Micro as it allows to keep our code base simple and clean.</p> <h3>Caliburn.Micro</h3> <p>Caliburn.Micro is a MVVM framework. It is a convention based framework and after you learn the conventions, it is powerful and simple to use. The reason we’re using Caliburn.Micro in this app is its built-in view locator. The view locator is the component which allows you to easily build UWP dashboard apps and which allows us to do that with minimum amount of code.</p> <h2>Creating our app</h2> <p>We will start by creating the app and configuring Caliburn.Micro. Starting point is the “Blank App (Universal Windows)” template:</p> <p><a href="http://mikaelkoskinen.net/posts/files/26536812-6ab8-4c1f-b7fe-34af1ebc1ebd.png"><img title="image" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="image" src="http://mikaelkoskinen.net/posts/files/7cd278da-e99d-481e-972c-82147e0f003c.png" width="414" height="354"></a></p> <p>Target and minimum versions don’t really matter:</p> <p><a href="http://mikaelkoskinen.net/posts/files/818b1fad-721f-4892-9f9d-ed40141325d7.png"><img title="image" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="image" src="http://mikaelkoskinen.net/posts/files/79c07423-78c3-4be0-bcaf-a3e485085bac.png" width="463" height="165"></a></p> <p>MainPage.xaml can be deleted at this point.</p> <p>To hook up Caliburn.Micro, first add the Nuget package “Caliburn.Micro” (version 3.2):</p> <p><a href="http://mikaelkoskinen.net/posts/files/a830092f-2afc-412a-a577-8f51c93cf134.png"><img title="image" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="image" src="http://mikaelkoskinen.net/posts/files/4c6416e9-e34f-459c-8e68-f29c4be0f3cd.png" width="644" height="177"></a></p> <p>Then open App.xaml and change it into the following format:</p><pre class="brush: xml; auto-links: true; collapse: false; first-line: 1; gutter: true; html-script: false; light: false; ruler: false; smart-tabs: true; tab-size: 4; toolbar: true;">&lt;cm:CaliburnApplication
x:Class="UWP_WidgetDasboard.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cm="using:Caliburn.Micro"
RequestedTheme="Light"&gt;

&lt;/cm:CaliburnApplication&gt;</pre>
<p>All the App.xaml.cs requires few changes: It doesn’t inherit Application anymore so we can remove that and also some Caliburn.Micro specific setup is needed. Once you get this right, you can usually just copy paste the code from app to app. The full App.xaml.cs can be found from this tutorial’s <a href="https://github.com/mikoskinen/blog/tree/master/UWP-WidgetDasboard" target="_blank">GitHub repository</a> but here’s the most interesting parts:</p><pre class="brush: csharp; auto-links: true; collapse: false; first-line: 1; gutter: true; html-script: false; light: false; ruler: false; smart-tabs: true; tab-size: 4; toolbar: true;"> protected override void Configure()
{
container = new WinRTContainer();
container.RegisterWinRTServices();

container.Singleton&lt;ShellViewModel&gt;();
}

protected override void OnLaunched(LaunchActivatedEventArgs args)
{
if (args.PreviousExecutionState == ApplicationExecutionState.Running)
return;

DisplayRootView&lt;ShellView&gt;();
}</pre>
<p>Configure-method will be used to initialize the widgets and the shell. OnLaunched decides page is shown at launch. As we can see the code uses ShellViewModel and ShellView so let’s create those before we can hit F5 to make sure everything works.</p>
<p>ShellView is just a blank page:</p>
<p><a href="http://mikaelkoskinen.net/posts/files/944270e9-e82a-42a0-b2f6-1f19375d2430.png"><img title="image" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="image" src="http://mikaelkoskinen.net/posts/files/7538d9d2-346a-406e-9318-7c8f201aeec7.png" width="441" height="334"></a></p>
<p>And ShellViewModel is a class which inherits Screen:</p><pre class="brush: csharp; auto-links: true; collapse: false; first-line: 1; gutter: true; html-script: false; light: false; ruler: false; smart-tabs: true; tab-size: 4; toolbar: true;">using Caliburn.Micro;

namespace UWP_WidgetDasboard
{
public class ShellViewModel : Screen
{
}
}
</pre>Now if you hit F5 you should see our empty app starting without errors:
<p><a href="http://mikaelkoskinen.net/posts/files/a46bb6b4-0d2c-4a4f-8c8b-3bf7b2c96dd5.png"><img title="image" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="image" src="http://mikaelkoskinen.net/posts/files/edc7142a-15cb-4207-964b-858f168aa931.png" width="576" height="266"></a></p>
<p>At this point we have a working app with a shell but without any content. The solution explorer is the following (highlighted are the files which we have changed):</p>
<p><a href="http://mikaelkoskinen.net/posts/files/e5ff01fc-d570-4d50-a7e3-cb6cfaccd628.png"><img title="image" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="image" src="http://mikaelkoskinen.net/posts/files/80d234f9-7274-4462-adf3-d93d0ea47557.png" width="310" height="258"></a></p>
<p>Next up is creating the widgets.</p>
<h2>Creating the first widget</h2>
<p>For each widget we need two things: </p>
<ul>
<li>UserControl for the UI</li>
<li>Caliburn.Micro based ViewModel (Screen or Conductor) for the logic</li></ul>
<p>We also have to have a common IWidget interface, which can be empty. This is our starting point:</p><pre class="brush: csharp; auto-links: true; collapse: false; first-line: 1; gutter: true; html-script: false; light: false; ruler: false; smart-tabs: true; tab-size: 4; toolbar: true;">namespace UWP_WidgetDasboard
{
public interface IWidget
{
}
}</pre>
<p>Let’s create our first widget: Customer Info. To get some structure, we’re going to create a folder for each widget. These aren’t required but they make things easier to find. Create an empty user control (CustomerInfoView) and an empty class (CustomerInfoViewModel) into the project:</p>
<p><a href="http://mikaelkoskinen.net/posts/files/d5967b62-0ae3-4c2c-98e3-81ea1d8df6a5.png"><img title="image" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="image" src="http://mikaelkoskinen.net/posts/files/2623d1d1-6f77-4b3d-981e-993467aaaac2.png" width="289" height="180"></a></p>
<p>CustomerInfoViewModel is the place where widget’s logic will be: It can contact the database/web api to fetch data or access some resource packaged with the app. The CustomerInfoViewModel should be a “Caliburn.Micro ViewModel”, meaning it needs to inherits from a Screen or from a Conductor. Screen is your choice if you want to display information about a single object, Conductor is great if you have a list of object. For example CustomerInfoViewModel is a Screen because we just display info about a single Customer. CustomerSearchViewModel should be a Conductor as it quite likely displays a range of customers.</p>
<p>To make things work, the widget’s view model should also implement our IWidget interface. When using Caliburn.Micro, the widget’s initialization logic can be placed inside the OnActivate-method. Let’s insert some logic:</p><pre class="brush: csharp; auto-links: true; collapse: false; first-line: 1; gutter: true; html-script: false; light: false; ruler: false; smart-tabs: true; tab-size: 4; toolbar: true;"> public class CustomerInfoViewModel : Screen, IWidget
{
private Customer _customer;

public Customer Customer
{
get { return _customer; }
set
{
_customer = value;
NotifyOfPropertyChange(() =&gt; Customer);
}
}

protected override void OnActivate()
{
this.Customer = new Customer("Mikael", "Koskinen");
}
}

public class Customer
{
public string FirstName { get; set; }
public string LastName { get; set; }

public Customer(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
}</pre>
<p>Now we just implement the UI for our widget to display customer information:</p><pre class="brush: xml; auto-links: true; collapse: false; first-line: 1; gutter: true; html-script: false; light: false; ruler: false; smart-tabs: true; tab-size: 4; toolbar: true;"> &lt;Grid&gt;

&lt;Grid.ColumnDefinitions&gt;
&lt;ColumnDefinition/&gt;
&lt;ColumnDefinition/&gt;
&lt;/Grid.ColumnDefinitions&gt;
&lt;Grid.RowDefinitions&gt;
&lt;RowDefinition/&gt;
&lt;RowDefinition/&gt;
&lt;RowDefinition/&gt;
&lt;/Grid.RowDefinitions&gt;

&lt;TextBlock Style="{StaticResource TitleTextBlockStyle}" Grid.ColumnSpan="2" Text="Customer Information:"/&gt;
&lt;TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Grid.Column="0" Grid.Row="1"&gt;First name:&lt;/TextBlock&gt;
&lt;TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Grid.Column="0" Grid.Row="2"&gt;Last name:&lt;/TextBlock&gt;

&lt;TextBlock Style="{StaticResource BodyTextBlockStyle}" Grid.Column="1" Grid.Row="1" Text="{Binding Customer.FirstName}"/&gt;
&lt;TextBlock Style="{StaticResource BodyTextBlockStyle}" Grid.Column="1" Grid.Row="2" Text="{Binding Customer.LastName}"/&gt;

&lt;/Grid&gt;</pre>

<p>Next up is making the shell work.</p>
<h2>Creating the shell</h2>
<p>The shell hosts the widgets and the common UI functions. We’re going the skip the common UI functions and just include a container which contains all the widgets.</p>
<p>The shell is where you decide how you want to represent the widgets: You can use similar square box containers as VSTS’ or you can host each widget in a tab control or hub or carousel or anything you like. We’re going to use the square box containers. </p>
<p>All the widgets are “injected” into the ShellViewModel using dependency injection. This way shell doesn’t need to know which widgets it is currently hosting. </p>
<p>To get the dependency injection working, we need to add all the classes implementing IWidget into the Caliburn.Micro’s DI container. This is done in App.xaml.cs:</p><pre class="brush: csharp; auto-links: true; collapse: false; first-line: 1; gutter: true; html-script: false; light: false; ruler: false; smart-tabs: true; tab-size: 4; toolbar: true;"> protected override void Configure()
{
container = new WinRTContainer();
container.RegisterWinRTServices();

container.Singleton&lt;ShellViewModel&gt;();
AddWidgets();
}

private void AddWidgets()
{
var types = typeof(ShellViewModel).GetTypeInfo().Assembly.GetTypes().ToList();
var widgets = types.Where(type =&gt; type.GetTypeInfo().IsClass &amp;&amp; System.Reflection.TypeExtensions.IsAssignableFrom(typeof(IWidget), type)).ToList();

foreach (var widget in widgets)
{
container.RegisterPerRequest(typeof(IWidget), null, widget);
}
}</pre>
<p>Now when we run the app, all the widgets implementing IWidget are automatically added inside the DI container. We can use this in our ShellViewModel. At this point we should also change ShellViewModel to inherit Conductor&lt;IWidget&gt;. This provides us Items-property, which we can use to add the widgets:</p><pre class="brush: csharp; auto-links: true; collapse: false; first-line: 1; gutter: true; html-script: false; light: false; ruler: false; smart-tabs: true; tab-size: 4; toolbar: true;"> public class ShellViewModel : Conductor&lt;IWidget&gt;.Collection.AllActive
{
public ShellViewModel(IEnumerable&lt;IWidget&gt; widgets)
{
this.Items.AddRange(widgets);
}
}</pre>
<p>Now we have the basic logic implemented and we just need to display the widgets inside the ShellView. For this you can use any container. We’re going to use ItemsControl with ItemsWrapGrid:</p><pre class="brush: xml; auto-links: true; collapse: false; first-line: 1; gutter: true; html-script: false; light: false; ruler: false; smart-tabs: true; tab-size: 4; toolbar: true;"> &lt;Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"&gt;
&lt;ItemsControl x:Name="Items"&gt;
&lt;ItemsControl.ItemsPanel&gt;
&lt;ItemsPanelTemplate&gt;
&lt;ItemsWrapGrid Orientation="Horizontal"/&gt;
&lt;/ItemsPanelTemplate&gt;
&lt;/ItemsControl.ItemsPanel&gt;
&lt;ItemsControl.ItemTemplate&gt;
&lt;DataTemplate&gt;
&lt;Border BorderThickness="2" BorderBrush="LightBlue" Margin="12" Width="400" Height="400"&gt;
&lt;Grid&gt;
&lt;Border Background="LightBlue" Opacity="0.1"/&gt;
&lt;ContentControl Margin="12" micro:View.Model="{Binding}" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" /&gt;
&lt;/Grid&gt;
&lt;/Border&gt;
&lt;/DataTemplate&gt;
&lt;/ItemsControl.ItemTemplate&gt;
&lt;/ItemsControl&gt;
&lt;/Grid&gt;</pre>
<p>Now when you run the application, you should see our single widget hosted inside the shell:</p>
<p><a href="http://mikaelkoskinen.net/posts/files/589239c0-3dbb-4af5-b04b-728c35490136.png"><img title="image" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="image" src="http://mikaelkoskinen.net/posts/files/57162e7b-b08c-45f9-9084-a6268b1c7fc5.png" width="380" height="214"></a></p>
<p>We can make sure everything works correctly by adding couple more widgets:</p>
<p><a href="http://mikaelkoskinen.net/posts/files/aa2b8b3f-382b-456a-b94d-403a99c7e7ce.png"><img title="image" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; margin: 0px; display: inline; padding-right: 0px" border="0" alt="image" src="http://mikaelkoskinen.net/posts/files/f905bae5-c359-49d4-b0fa-7684e0af4a9f.png" width="244" height="234"></a></p>
<p><a href="http://mikaelkoskinen.net/posts/files/10e6e103-2edc-4eae-9fe6-0833f81bfa4d.png"><img title="image" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="image" src="http://mikaelkoskinen.net/posts/files/e33bf02c-ff7b-4c5c-8289-59e8d0c6b9e3.png" width="593" height="424"></a></p>
<h2>Conclusion and further work:</h2>
<p>This tutorial shows what it takes to create UWP dashboard widget app. It doesn’t take much: Only few lines of code is required to for the shell and IWidget, after which all the coding takes place inside the widgets. As each widget can be as complicated or as simple as they need, bulk of your code will be place into the individual widgets.</p>
<p>To make this full blown solution, the widget system needs some more functionality:</p>
<ul>
<li>Instead of always having all the widgets, user should be able to add and remove widgets. </li>
<li>Common widget elements, like title and action buttons.</li>
<li>Different sized widgets instead of a single fixed size.</li>
<li>Ability to send notifications from a widget to a widget using Event aggregator.</li>
<li>Ability to send notifications from a widget to the shell using Event aggregator.</li></ul>
<p>The full source code can be found from the GitHub: <a title="https://github.com/mikoskinen/blog/tree/master/UWP-WidgetDasboard" href="https://github.com/mikoskinen/blog/tree/master/UWP-WidgetDasboard">https://github.com/mikoskinen/blog/tree/master/UWP-WidgetDasboard</a></p>