Using IoC and Design time support with MVVM Light and Template 10

23/09/2015 3 comments

In the last post we talked about Integrating Template10 with MVVM Light. Now let’s see how to use some basic services provided by MVVM Light, starting with SimpleIoC and Design time support.

In the previous post we already used it to register a simple ViewModel, but it is more powerful, and for example we can leverage it to inject dependencies. For example, let’s define the following service:

public interface IDataService
{
    IEnumerable<Person> GetPeople();
}

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

public class DesignTimeDataService : IDataService
{
    public IEnumerable<Person> GetPeople()
    {
        var people = from n in Enumerable.Range(1, 100)
                     select new Person
                     {
                         FirstName = "Design Time First Name " + n,
                         LastName = "Design Time Last Name " + n
                     };

        return people;
    }
}

public class RuntimeDataService : IDataService
{
    public IEnumerable<Person> GetPeople()
    {
        var people = new List<Person>();

        // In a real scenario, these people are retrived for example via 
        // a Web API.
        people.Add(new Person { FirstName = "Donald", LastName = "Duck" });
        people.Add(new Person { FirstName = "Mickey", LastName = "Mouse" });
        people.Add(new Person { FirstName = "Daisy", LastName = "Duck" });

        return people;
    }
}

The IDataService interface defines a method the retrieves a list of people. Then we have a design and a runtime implementation. We need to register them in the ViewModelLocator class:

public class ViewModelLocator
{
    static ViewModelLocator()
    {
        ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
 
        if (GalaSoft.MvvmLight.ViewModelBase.IsInDesignModeStatic)
        {
            // We are at design time.
            SimpleIoc.Default.Register<IDataService, DesignTimeDataService>();
        }
        else
        {
            // We are at runtime.
            SimpleIoc.Default.Register<IDataService, RuntimeDataService>();
        }
 
        SimpleIoc.Default.Register<MainViewModel>();
    }
 
    // ...
}

At line 7 we use the MVVM Light ViewModelBase.IsInDesignModeStatic property to check if we are at design time (in fact, since the ViewModelLocator is declared as a resource in the App.xaml file, it is automatically instantiated, even if the app isn’t explicitly executed). If so, we register in the MVVM Light SimpleIoC container the type DesignTimeDataService for the interface IDataService. Otherwise, if we are at execution time we register the type RuntimeDataService.

Now we can pass an object implementing the IDataService interface to our View Model:

public class MainViewModel : Template10.Mvvm.ViewModelBase
{
    private IEnumerable<Person> people;
    public IEnumerable<Person> People
    {
        get { return people; }
        set { this.Set(ref people, value); }
    }

    private readonly IDataService dataService;

    public MainViewModel(IDataService dataService)
    {
        this.dataService = dataService;

        if (GalaSoft.MvvmLight.ViewModelBase.IsInDesignModeStatic)
        {
            // Loads people to enable desgin time support.
            People = dataService.GetPeople();
        }
    }

    public override void OnNavigatedTo(object parameter, NavigationMode mode, 
        IDictionary<string, object> state)
    {
        // Loads real data.
        People = dataService.GetPeople();

        base.OnNavigatedTo(parameter, mode, state);
    }
}

In this way, when we are at design time, the type passed to MainViewModel is DesignTimeDataService, while at execution time we’ll have a RuntimeDataService. In fact, in the MainViewModel constructor (lines 12-20) we check whether we are actually at design time, and so we call the GetPeople method, in order to show dummy data (from DesignTimeDataService) to enable design time support. Note that we again use a method available in the ViewModelBase class that comes from MVVM Light, and not the Template10 ViewModelBase implementation from which the View model inherits:

Design Time data

Design Time data

Finally, in the Template 10 OnNavigatedTo method (lines 23-30), we call again GetPeople: as this method is executed when we are at runtime, now we’re retrieving information from RuntimeDataService:

Runtime data

Runtime data

Working with interfaces and implementations is strongly recommended. Besides allowing design time support, as we have just seen, it make easier to test our code. MVVM Light provides all the tools that we need to do that (like a simple IoC container), and integrating it in a project built with Template 10 is straightforward.

You can download the complete example using the link below:
Using IoC with MVVM Light and Template10

Advertisements

Integrating Template10 with MVVM Light

15/09/2015 4 comments

Template10 is an advanced project template for the Unversal Windows Platform. It’s an open source project available on GitHub at https://github.com/Windows-XAML/Template10. If you don’t know about it, I suggest you to read the great article Template10: a new template to create Universal Windows apps – The basics by Matteo Pagani (Support Engineer at Microsoft).

The template has been created with MVVM in mind, and in fact contains some useful classes that simplify the adoption of this pattern (like BindableBase and ViewModelBase). But it isn’t an MVVM Framework (or toolkit). However, it’s easy to integrate Template10 with, for example, MVVM Light. Let’s see how to do that.

First of all, let’s add the template to our app, as described in the post linked above (you can also find it on NuGet). Then, we need to add MVVM Light using NuGet: search for the MvvmLightLibs package and install it. As NuGet 3.0 doesn’t allow anymore to run installation script, even if we choose the MvvmLight package, it will not install additional files like ViewModelLocator, nor it will modify the App.xaml file with its reference. So, we need to perform these steps manually.

Let’s create a folder called ViewModels with a MainViewModel.cs file:

public class MainViewModel : Template10.Mvvm.ViewModelBase
{
    public string HelloMessage { get; }

    public MainViewModel()
    {
        HelloMessage = "Hello from Mvvm!";
    }

    public override void OnNavigatedTo(object parameter, NavigationMode mode, 
        IDictionary<string, object> state)
    {
        base.OnNavigatedTo(parameter, mode, state);
    }
}

Note that MainViewModel inherits from a class contained in the Template10 (that, among the others, define the OnNavigatedTo method: as we’ll see, it we’ll be invoked when the user navigates to the associated page.

Now we need to implement a ViewModelLocator:

public class ViewModelLocator
{
    static ViewModelLocator()
    {
        ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);

        SimpleIoc.Default.Register<MainViewModel>();
    }

    public MainViewModel MainViewModel 
        => ServiceLocator.Current.GetInstance<MainViewModel>();
}

Finally, let’s insert the ViewModelLocator in the Application Resources (App.xaml):

<common:BootStrapper
    ...>

    <Application.Resources>
        <ResourceDictionary>
            <vm:ViewModelLocator x:Key="Locator" 
                            xmlns:vm="using:Template10MvvmLight.ViewModels" />
        </ResourceDictionary>
    </Application.Resources>
    
</common:BootStrapper>

And set the DataContext of MainPage.xaml:

<Page
    x:Class="Template10MvvmLight.MainPage"
    DataContext="{Binding Source={StaticResource Locator}, Path=MainViewModel}"
    ...>

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBlock Text="{Binding HelloMessage}" HorizontalAlignment="Center"
                   VerticalAlignment="Center" />
    </Grid>
</Page>

We can now test the app. Set a breakpoint in the OnNavigatedTo method of the MainViewModel class and run the project: as we expect, it will be called on page activation and then the TextBlock will show the HelloMessage property of the view model.

You can download the complete example using the link below:
Integrating Template10 with MVVM Light

In the next post, we’ll see how to use the other services provided by MVVM Light, so stay tuned!

FullScreenModeTrigger for Universal Windows Platform

14/07/2015 2 comments

Last week we talked about full-screen mode in Windows 10 Universal apps. In this scenario, we could have the need to adjust the UI when we enter full-screen mode. The best way to do so is to create an Adaptive Trigger:

public class FullScreenModeTrigger : StateTriggerBase
{
    public bool IsFullScreen
    {
        get { return (bool)GetValue(IsFullScreenProperty); }
        set { SetValue(IsFullScreenProperty, value); }
    }

    public static readonly DependencyProperty IsFullScreenProperty =
        DependencyProperty.Register("IsFullScreen", typeof(bool), 
        typeof(FullScreenModeTrigger),
        new PropertyMetadata(false, OnIsFullScreenPropertyChanged));

    private static void OnIsFullScreenPropertyChanged(DependencyObject d, 
        DependencyPropertyChangedEventArgs e)
    {
        var obj = (FullScreenModeTrigger)d;
        if (!Windows.ApplicationModel.DesignMode.DesignModeEnabled)
        {
            var isFullScreen = ApplicationView.GetForCurrentView().IsFullScreenMode;
            obj.UpdateTrigger(isFullScreen);
        }
    }

    public FullScreenModeTrigger()
    {
        if (!Windows.ApplicationModel.DesignMode.DesignModeEnabled)
            ApplicationView.GetForCurrentView().VisibleBoundsChanged += 
                FullScreenModeTrigger_VisibleBoundsChanged;
    }

    private void FullScreenModeTrigger_VisibleBoundsChanged(ApplicationView sender, 
        object args)
    {
        UpdateTrigger(sender.IsFullScreenMode);
    }

    private void UpdateTrigger(bool isFullScreen)
    {
        SetActive(isFullScreen == IsFullScreen);
    }
}

It defines a dependency property (line 9-12) that specifies if we want to activate the trigger when in full-screen or windowed mode. Then, in the constructor we register on the ApplicationView.VisibleBoundsChanged event (line 28-29). In its event handler (lines 32-36), we check ApplicationView.IsFullScreenMode and activate the trigger based on the value of the IsFullScreen property.

Now we can use this trigger in XAML:

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
  <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
      <TextBlock x:Name="fullScreenModeStatus"  
                 HorizontalAlignment="Center" 
                 VerticalAlignment="Center" />
      <Button x:Name="fullScreenMode" Content="Toggle Full Screen Mode" 
              Click="fullScreenMode_Click" />
  </StackPanel>

  <VisualStateManager.VisualStateGroups>
      <VisualStateGroup >
          <VisualState x:Name="fullScreen">
              <VisualState.StateTriggers>
                  <triggers:FullScreenModeTrigger IsFullScreen="true" />
              </VisualState.StateTriggers>
              <VisualState.Setters>
                  <Setter Target="fullScreenModeStatus.Text" 
                          Value="App is in full screen mode" />
                  <Setter Target="fullScreenMode.Content" Value="Exit full screen" />
              </VisualState.Setters>
          </VisualState>
          <VisualState x:Name="windowed">
              <VisualState.StateTriggers>
                  <triggers:FullScreenModeTrigger IsFullScreen="false" />
              </VisualState.StateTriggers>
              <VisualState.Setters>
                  <Setter Target="fullScreenModeStatus.Text" 
                          Value="App is in windowed mode" />
                  <Setter Target="fullScreenMode.Content" Value="Enter full screen" />
              </VisualState.Setters>
          </VisualState>
      </VisualStateGroup>
  </VisualStateManager.VisualStateGroups>
</Grid>

We have defined two visual states (lines 12-21 and 22-32), that are activated according to the value of the IsFullScreen property (lines 14 and 24).

Finally, in the code behind of the page let’s insert the code to enter and exit the full-screen mode:

private void fullScreenMode_Click(object sender, RoutedEventArgs e)
{
    var view = ApplicationView.GetForCurrentView();
    var isFullScreenMode = view.IsFullScreenMode;

    if (isFullScreenMode)
        view.ExitFullScreenMode();
    else
        view.TryEnterFullScreenMode();
}

We simply use a button to toggle the full-screen mode, as described in the previous post.

Full Screen Mode in Windows 10 Universal apps

09/07/2015 2 comments

Windows 8/8.1 apps on a Windows 10 Desktop are shown in window, as usual. But, in this case, on the title bar appears a button that allows to put the app in full-screen mode, to resemble the old style.

In fact, we can leverage a new API to show our apps in full-screen mode even if we’re using the new Universal Windows Platform. Note that in this case we haven’t a built-in command to enter/exit this mode, so we need to provide it in our apps.

So, for example, we can write something like this:

private async void toggleFullScreenButton_Click(object sender, RoutedEventArgs e)
{
    var view = ApplicationView.GetForCurrentView();
    if (view.IsFullScreenMode)
    {
        view.ExitFullScreenMode();
    }
    else
    {
        var succeeded = view.TryEnterFullScreenMode();
        if (!succeeded)
        {
            var dialog = new MessageDialog("Unable to enter the full-screen mode.");
            await dialog.ShowAsync();
        }
    }
}

First of all, we check if the app is already in full-screen mode (line 4). If so, we call the ExitFullScreenMode method (line 6) to return to the standard windowed mode. Otherwise, we call TryEnterFullScreenMode (line 10) to attempt to place the app in full-screen mode. This method can fail, so we check its return value to determine whether the app is actually placed in full-screen mode.

Note that the full-screen APIs is available also on phones. In this case, a full-screen app has no statusbar and no “soft navigation buttons”.

EnumStateTrigger for Universal Windows Platform

07/07/2015 1 comment

The new Universal Windows Platform introduces Adaptive Triggers, that allow to use declaratives rules in XAML to define transitions between visual states, as described in a previous post.

The most interesting thing about this new concept is that we can define our triggers to handle specific scenarios: we simply need to inherit from the StateTriggerBase class and call its SetActive method to activate it.

Suppose for example that we want to show or hide certain UI elements based on some access rules, i.e., if the current user is logged or anonymous, or if he has read/write rights, etc. In this case, we can define an EnumStateTrigger like the following one:

public class EnumStateTrigger : StateTriggerBase
{
    public object Value
    {
        get { return (object)GetValue(ValueProperty); }
        set { SetValue(ValueProperty, value); }
    }

    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register("Value", typeof(object), 
        typeof(EnumStateTrigger),
        new PropertyMetadata(null, ValuePropertyChanged));

    public string ActiveValues
    {
        get { return (string)GetValue(ActiveValuesProperty); }
        set { SetValue(ActiveValuesProperty, value); }
    }

    public static readonly DependencyProperty ActiveValuesProperty =
        DependencyProperty.Register("ActiveValues", typeof(string), 
        typeof(EnumStateTrigger),
        new PropertyMetadata(null, ValuePropertyChanged));

    private static void ValuePropertyChanged(object sender,
        DependencyPropertyChangedEventArgs e)
    {
        var obj = (EnumStateTrigger)sender;
        obj.UpdateTrigger();
    }

    private void UpdateTrigger()
    {
        if (Value == null || ActiveValues == null)
        {
            SetActive(false);
        }
        else
        {
            var currentStates = Value.ToString().ToLower()
                .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
                .Select(s => s.Trim()).ToList();

            var stateStrings = ActiveValues.ToString().ToLower()
                .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
                .Select(s => s.Trim()).ToList();

            var isActive = currentStates.Intersect(stateStrings).Any();
            SetActive(isActive);
        }
    }
}

It defines two dependency properties: Value is the current value that must be checked, while ActiveValues are comma separated enum values that activate the trigger. Then, in the UpdateTrigger method (line 32-51), we check if current values interesect active values and activate the trigger correspondingly. We use this tecnique to handle also the | operator for enum flags.

Now we can use this trigger in XAML:

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
        <StackPanel Orientation="Horizontal" Margin="0,0,0,10">
            <Button x:Name="accessLevelNone" Content="None" 
                    Click="accessLevelNone_Click" />
            <Button x:Name="accessLevelRead" Content="Read"
                    Click="accessLevelRead_Click" />
            <Button x:Name="accessLevelWrite" Content="Write"
                    Click="accessLevelWrite_Click" />
        </StackPanel>

        <TextBlock x:Name="enumStatus"         
                   HorizontalAlignment="Center" 
                   VerticalAlignment="Center" />
    </StackPanel>

    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup >
            <VisualState x:Name="none">
                <VisualState.StateTriggers>
                    <triggers:EnumStateTrigger 
                        Value="{x:Bind SelectedAccessLevel, Mode=OneWay}" 
                        ActiveValues="None" />
                </VisualState.StateTriggers>
                <VisualState.Setters>
                    <Setter Target="enumStatus.Text" Value="Access level: none" />
                    <Setter Target="accessLevelNone.FontWeight" Value="Bold" />
                </VisualState.Setters>
            </VisualState>
            <VisualState x:Name="readerOrWriter">
                <VisualState.StateTriggers>
                    <triggers:EnumStateTrigger 
                        Value="{x:Bind SelectedAccessLevel, Mode=OneWay}" 
                        ActiveValues="Reader, Writer" />
                </VisualState.StateTriggers>
                <VisualState.Setters>
                    <Setter Target="enumStatus.Text" 
                            Value="Access level: reader or writer" />
                    <Setter Target="accessLevelRead.FontWeight" Value="Bold" />
                    <Setter Target="accessLevelWrite.FontWeight" Value="Bold" />
                </VisualState.Setters>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
</Grid>

We have defined two visual states (lines 19-29 and 30-42), that are activated according to the value of the bound SelectedAccessLevel property (lines 21-23 and 32-34): the former is related to the None enum value, the latter to Reader or Writer.

And finally the code-behind for the page:

public sealed partial class MainPage : Page, INotifyPropertyChanged
{
    public MainPage()
    {
        this.InitializeComponent();
    }

    private AccessLevel selectedAccessLevel = AccessLevel.None;
    public AccessLevel SelectedAccessLevel
    {
        get { return selectedAccessLevel; }
        set
        {
            if (selectedAccessLevel != value)
            {
                selectedAccessLevel = value;
                PropertyChanged?.Invoke(this, 
                    new PropertyChangedEventArgs(nameof(SelectedAccessLevel)));
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void accessLevelNone_Click(object sender, RoutedEventArgs e)
    {
        SelectedAccessLevel = AccessLevel.None;
    }

    private void accessLevelRead_Click(object sender, RoutedEventArgs e)
    {
        SelectedAccessLevel = AccessLevel.Reader;
    }

    private void accessLevelWrite_Click(object sender, RoutedEventArgs e)
    {
        SelectedAccessLevel = AccessLevel.Writer;
    }
}

public enum AccessLevel
{
    None,
    Reader,
    Writer
}

When we click one the the buttons (line 25-38), the SelectedAccessLevel property changes, so triggers that are bound to it are re-evaluated, causing the transition to the corresponding visual states.

Easily manage the Title Bar in Windows 10 apps

19/05/2015 2 comments

Windows 10 apps run in a window like any standard desktop application. This means that now they have a title bar, that by default is gray, has the name of the app on the left and the standard three button on the right. However, if we need, we can customize its appearance and colors.

To reach this goal, we can leverage the ApplicationView.GetForCurrentView().TitleBar property. So, we need to do this via code. For example:

var titleBar = ApplicationView.GetForCurrentView().TitleBar;
titleBar.BackgroundColor = Colors.DarkBlue;
titleBar.ForegroundColor = Colors.White;
titleBar.ButtonBackgroundColor = Colors.Maroon;
titleBar.ButtonForegroundColor = Colors.Yellow;

With these instructions, we obtain the following chrome:

Windows 10 app Title Bar with custom chrome

Windows 10 app Title Bar with custom chrome

This solution works great, but we can improve it by defining the appropriate attached properties on the Page class, so that we can customize Title Bar colors via XAML:

public static class TitleBar
{
    public static readonly DependencyProperty ForegroundColorProperty =
        DependencyProperty.RegisterAttached("ForegroundColor", typeof(Color),
        typeof(TitleBar), 
        new PropertyMetadata(null, OnForegroundColorPropertyChanged));

    public static Color GetForegroundColor(DependencyObject d)
    {
        return (Color)d.GetValue(ForegroundColorProperty);
    }

    public static void SetForegroundColor(DependencyObject d, Color value)
    {
        d.SetValue(ForegroundColorProperty, value);
    }

    private static void OnForegroundColorPropertyChanged(DependencyObject d,
        DependencyPropertyChangedEventArgs e)
    {
        var color = (Color)e.NewValue;
        var titleBar = ApplicationView.GetForCurrentView().TitleBar;
        titleBar.ForegroundColor = color;
    }

    public static readonly DependencyProperty BackgroundColorProperty =
        DependencyProperty.RegisterAttached("BackgroundColor", typeof(Color),
        typeof(TitleBar), 
        new PropertyMetadata(null, OnBackgroundColorPropertyChanged));

    public static Color GetBackgroundColor(DependencyObject d)
    {
        return (Color)d.GetValue(BackgroundColorProperty);
    }

    public static void SetBackgroundColor(DependencyObject d, Color value)
    {
        d.SetValue(BackgroundColorProperty, value);
    }

    private static void OnBackgroundColorPropertyChanged(DependencyObject d,
        DependencyPropertyChangedEventArgs e)
    {
        var color = (Color)e.NewValue;
        var titleBar = ApplicationView.GetForCurrentView().TitleBar;
        titleBar.BackgroundColor = color;
    }

    public static readonly DependencyProperty ButtonForegroundColorProperty =
        DependencyProperty.RegisterAttached("ButtonForegroundColor", typeof(Color),
        typeof(TitleBar), 
        new PropertyMetadata(null, OnButtonForegroundColorPropertyChanged));

    public static Color GetButtonForegroundColor(DependencyObject d)
    {
        return (Color)d.GetValue(ButtonForegroundColorProperty);
    }

    public static void SetButtonForegroundColor(DependencyObject d, Color value)
    {
        d.SetValue(ButtonForegroundColorProperty, value);
    }

    private static void OnButtonForegroundColorPropertyChanged(DependencyObject d,
        DependencyPropertyChangedEventArgs e)
    {
        var color = (Color)e.NewValue;
        var titleBar = ApplicationView.GetForCurrentView().TitleBar;
        titleBar.ButtonForegroundColor = color;
    }

    public static readonly DependencyProperty ButtonBackgroundColorProperty =
        DependencyProperty.RegisterAttached("ButtonBackgroundColor", typeof(Color),
        typeof(TitleBar), 
        new PropertyMetadata(null, OnButtonBackgroundColorPropertyChanged));

    public static Color GetButtonBackgroundColor(DependencyObject d)
    {
        return (Color)d.GetValue(ButtonBackgroundColorProperty);
    }

    public static void SetButtonBackgroundColor(DependencyObject d, Color value)
    {            
        d.SetValue(ButtonBackgroundColorProperty, value);
    }

    private static void OnButtonBackgroundColorPropertyChanged(DependencyObject d,
        DependencyPropertyChangedEventArgs e)
    {
        var color = (Color)e.NewValue;
        var titleBar = ApplicationView.GetForCurrentView().TitleBar;
        titleBar.ButtonBackgroundColor = color;
    }
}

These attached properties allow to set the same properties we have seen before, but now directly in XAML:

<Page
    x:Class="FirstDemo.MainPage"
    local:TitleBar.BackgroundColor="DarkBlue"
    local:TitleBar.ForegroundColor="White"
    local:TitleBar.ButtonBackgroundColor="Maroon"
    local:TitleBar.ButtonForegroundColor="Yellow"
    ...>

It’s more elegant and removes the need of code-behind. Note that other properties are available:

  • ButtonHoverBackgroundColor
  • ButtonHoverForegroundColor
  • ButtonInactiveBackgroundColor
  • ButtonInactiveForegroundColor
  • ButtonPressedBackgroundColor
  • ButtonPressedForegroundColor
  • InactiveBackgroundColor
  • InactiveForegroundColor

Using the same approach we have just analyzed, we can easily add the corresponding attached properties.

But we can do more. First of all, we can extend the UI into the Title Bar, making it a “chromeless” app. We need just one line of code:

CoreApplication.GetCurrentView().TitleBar.ExtendViewIntoTitleBar = true;

Note that in this case we are using CoreApplication.GetCurrentView, while in the previous snippet we set colors with ApplicationView.GetForCurrentView. In particular, we change the value of the ExtendViewIntoTitleBar property.

However, if we remove the title bar at all, we can’t move the window around. Actually, there is an area immediately to the left of title bar buttons which can be grabbed to drag the window, but it’s quite small and users will not expect this behavior.

The correct solution is to use the Window.Current.SetTitleBar method to set an element to act like the default title bar. So, let’s try to add this feature with a Behavior. The first thing to do is to add a reference to the Behavior SDK:

Adding the Behavior SDK to the project

Adding the Behavior SDK to the project

For the moment, we can ignore the compatibility warning. Now define the TitleBarBehavior:

public class TitleBarBehavior : DependencyObject, IBehavior
{
    public DependencyObject AssociatedObject { get; private set; }

    public void Attach(DependencyObject associatedObject)
    {
        var newTitleBar = associatedObject as UIElement;
        if (newTitleBar == null)
            throw new ArgumentException(
                "TitleBarBehavior can be attached only to UIElement");

        Window.Current.SetTitleBar(newTitleBar);
    }

    public void Detach() { }

    public bool IsChromeless
    {
        get { return (bool)GetValue(IsChromelessProperty); }
        set { SetValue(IsChromelessProperty, value); }
    }

    public static readonly DependencyProperty IsChromelessProperty =
        DependencyProperty.Register("IsChromeless",
        typeof(bool),
        typeof(TitleBarBehavior),
        new PropertyMetadata(false, OnIsChromelessChanged));

    private static void OnIsChromelessChanged(DependencyObject d,
        DependencyPropertyChangedEventArgs e)
    {
        CoreApplication.GetCurrentView().TitleBar
            .ExtendViewIntoTitleBar = (bool)e.NewValue;
    }
}

In the Attach method (lines 5-13), we verify that the associated object is a UIElement and, if so, we invoke the Window.Current.SetTitleBar method to use it as Title Bar. Now let’s take a look to the following XAML:

<Page
    ...
    xmlns:i="using:Microsoft.Xaml.Interactivity"
    ...>

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Border x:Name="customTitleBar" VerticalAlignment="Top" Height="32">
            <Border.Background>
                <LinearGradientBrush>
                    <GradientStop Color="Blue" Offset="0" />
                    <GradientStop Color="WhiteSmoke" Offset="1.0" />
                </LinearGradientBrush>
            </Border.Background>
            <StackPanel Margin="12,5,5,5" Orientation="Horizontal">
                <FontIcon FontFamily="Segoe MDL2 Assets" Glyph="" 
                          Foreground="White" VerticalAlignment="Center" Margin="0,0,8,0" />
                <TextBlock Text="My awesome app title" Foreground="White"
                           VerticalAlignment="Center"/>
            </StackPanel>

            <i:Interaction.Behaviors>
                <local:TitleBarBehavior IsChromeless="True"/>
            </i:Interaction.Behaviors>
        </Border>
        <Grid Grid.Row="1">
            ...
        </Grid>
    </Grid>
</Page>

Note that the TitleBarBehavior is attached to the customTitleBar object (lines 25-28). Running the app, we’ll see the following output:

Our app with a custom title bar

Our app with a custom title bar

In this way, Windows will handle input to the title bar XAML, so that we can move the window by dragging the elements in the title bar, or invoke the title bar context menu by right-clicking.

Adaptive Triggers, RelativePanel and DataTemplate in the Universal Windows Platform

12/05/2015 5 comments

When developing for the Universal Windows Platform, we can use Adaptive Triggers and controls like the RelativePanel to dynamically tailor the UI of our apps according to various conditions, in particular the screen resolution. We can for example modify the dimension of an image, the font size of a text, the margin between objects and rearrange them as the screen space changes.

Let’s see a simple example:

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <RelativePanel>
        <Image x:Name="world" Source="/Assets/World.jpg" Height="250" />
        <TextBlock x:Name="welcomeMessage" RelativePanel.Below="world"
                   RelativePanel.AlignHorizontalCenterWith="world"
                   FontSize="28" Text="Hello World!" />
    </RelativePanel>

    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup>
            <VisualState x:Name="narrowView">
                <VisualState.StateTriggers>
                    <AdaptiveTrigger MinWindowWidth="0" />
                </VisualState.StateTriggers>
                <VisualState.Setters>
                    <Setter Target="world.Height" Value="100" />
                    <Setter Target="welcomeMessage.FontSize" Value="14" />
                    <Setter Target="welcomeMessage.(RelativePanel.RightOf)"
                            Value="world" />
                    <Setter 
                        Target="welcomeMessage.(RelativePanel.AlignVerticalCenterWith)"
                        Value="world" />
                    <Setter 
                        Target="welcomeMessage.(RelativePanel.AlignHorizontalCenterWith)"
                        Value="{x:Null}" />
                </VisualState.Setters>
            </VisualState>
            <VisualState x:Name="wideView">
                <VisualState.StateTriggers>
                    <AdaptiveTrigger MinWindowWidth="720" />
                </VisualState.StateTriggers>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
</Grid>

The default UI contains an Image with an Height of 150 pixels (line 3) and a TextBlock with a FontSize of 28 (lines 4-6). These two controls are put inside a RelativePanel (lines 2-7): using the attached properties RelativePanel.Below and RelativePanel.AlignHorizontalCenterWith on the TextBlock (line 4-5), it is placed below the Image and horizontally centered:

The UI when effective pixels are >= 720

The UI when effective pixels are >= 720

Then, in the VisualStateManager section (lines 9-34), we have defined two states based on AdaptiveTrigger.MinWindowWidth condition.

The first one, narrowView, is applied when the screen width is between 0 (line 13) and 719 effective pixels (that is the lower bound limit defined in the next trigger, line 30). In this case, the Image height is set to 150 pixels, the font size of the TextBlock becomes 14 and the RelativePanel attached properties are modified so that the text appears vertically centered on the right of the image:

The UI when effective pixels are < 720

The UI when effective pixels are < 720

The other state, wideView, is used when the effective pixels are grater than or equal to 720. We haven’t defined any Setters, so the original properties are applied. In other words, the UI will be the default one we have defined directly in XAML (see first screenshot).

This approach works great and allows to easily adapt the UI according to the device characteristics. However, there is a particular situation we must keep in mind: we can’t directly use Adaptive Triggers with objects contained in a DataTemplate. Suppose we have the following ListView:

<ListView x:Name="peopleView" SelectionMode="None">
    <ListView.ItemTemplate>
        <DataTemplate>
            <RelativePanel Margin="8">
                <Image x:Name="personImage" Width="120" Height="120" Margin="12"
                       Source="{Binding ImageUrl}" />
                <TextBlock x:Name="personName" RelativePanel.Below="personImage" 
                       RelativePanel.AlignHorizontalCenterWith="personImage"
                       Text="{Binding FullName}"
                       FontSize="16" />
            </RelativePanel>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

If we want to change properties of personImage and personName based of screen size, we can’t just add a VisualStateManger inside the DataTemplate. Instead, we need to create a User Control and put the DataTemplate content inside it:

<UserControl
    x:Class="FirstDemo.PersonItem"
    ...>

    <RelativePanel Margin="8">
        <Image x:Name="personImage" Width="120" Height="120" Margin="12"
                                       Source="{Binding ImageUrl}" />
        <TextBlock x:Name="personName" RelativePanel.Below="personImage" 
                   RelativePanel.AlignHorizontalCenterWith="personImage"
                   Text="{Binding FullName}"
                   FontSize="16" />

        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup>
                <VisualState x:Name="narrowView">
                    <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="0" />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <Setter Target="personImage.Width" Value="64" />
                        <Setter Target="personImage.Height" Value="64" />
                        <Setter Target="personName.FontSize" Value="12" />
                        <Setter Target="personName.(RelativePanel.RightOf)" 
                                Value="personImage" />
                        <Setter 
                            Target="personName.(RelativePanel.AlignVerticalCenterWith)" 
                            Value="personImage" />
                        <Setter 
                            Target="personName.(RelativePanel.AlignHorizontalCenterWith)" 
                            Value="{x:Null}" />
                    </VisualState.Setters>
                </VisualState>
                <VisualState x:Name="wideView">
                    <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="720" />
                    </VisualState.StateTriggers>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
    </RelativePanel>
</UserControl>

In this way, we can use Adaptive Triggers as we see in our first example to dynamically change the appearance of all the items in the ListView (lines 13-39). Finally, let’s update the ListView definition to use the User Control:

<ListView x:Name="peopleView" SelectionMode="None">
    <ListView.ItemTemplate>
        <DataTemplate>
            <local:PersonItem />
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView> 

And now everything will work as expected.