Showing posts with label Behaviour. Show all posts
Showing posts with label Behaviour. Show all posts

11 February 2011

A Blendable Windows Phone 7 / Silverlight clipping behavior

If you are moving images around on a Canvas, you don’t want to show them when they get, for example, a negative Y-coordinate – for else they move outside of the Canvas and over what is above that. I’ve seen numerous examples of clipping ‘utilities’ that basically all ape this article on The Code Project. For some reason no-one seems to have thought about making it a real behavior that can be used from Blend. So after a short discussion over Twitter and DM with Thomas Woo (aka @codechinchilla) I decide to do my own take.

It’s not exactly rocket science:

using System.Windows;
using System.Windows.Media;
using System.Windows.Interactivity;

namespace LocalJoost.Behavior
{
  public class ClipToBoundsBehavior: Behavior<FrameworkElement>
  {
    protected override void OnAttached()
    {
      base.OnAttached();
      AssociatedObject.SizeChanged += AssociatedObjectSizeChanged;
      AssociatedObject.Loaded += AssociatedObjectLoaded;
    }

    protected override void OnDetaching()
    {
      AssociatedObject.SizeChanged -= AssociatedObjectSizeChanged;
      AssociatedObject.Loaded -= AssociatedObjectLoaded;
      base.OnDetaching();
    }

    void AssociatedObjectLoaded(object sender, RoutedEventArgs e)
    {
      SetClip();
    }

    void AssociatedObjectSizeChanged(object sender, SizeChangedEventArgs e)
    {
      SetClip();
    }

    private void SetClip()
    {
      AssociatedObject.Clip = new RectangleGeometry
      {
        Rect = new Rect(0, 0, 
          AssociatedObject.ActualWidth, AssociatedObject.ActualHeight)
      };
    }
  }
}

…and it’s even less code than the original sample. Drag this thingie in Blend on your Canvas - or whatever other framework element whose children you want to display only within its bounds - and you’re done. Or your designer is done.

I made this for a little Windows Phone 7 App I am currently working on, but it will pretty much work in plain Silverlight, and I am pretty sure it will work on WPF (and thus Microsoft Surface) as well.

06 October 2010

Using MVVMLight Messenger and behaviors to open files from local disk in Silverlight

The very first Silverlight application I had to make professionally (i.e. not as a hobby/research project at home) required the possibility to open a file from (local) disk. Of course I wanted to use Laurent Bugnion’s MVVMLight and the strict separation between logic that I talked and blogged about so much – ‘practice what thou preach’, eh?

This proved to be an interesting challenge. The most logical way forward seemed to be: make a button, attach a command to it via the usual EventToCommand route and then use OpenFileDialog from the model – which landed me a “Dialogs must be user-initiated” error. Googling Binging around I found out that Dennis Vroegop had run into this thingy quite recently as well.

Things looked ugly. Getting around it took me some code in the code behind. This kept nagging me, and only recently I found a better way to deal with this. Apart from the EventToCommand binding MVVMLight contains another gem, called the Messenger. This is an implementation of the Mediator pattern that - as far as I understand – is designed to broadcast property changes between different models in the application. But this mechanism can also be used as an alternative way to shuttle data from the GUI to the model in a designer-friendly – Blendable -way. The way to go for this particular problem is like this:

  • Define a message that holds the data from an OpenFileDialog
  • Make a behavior that can attach to a button that launches an OpenFileDialog on click, and then sends a message on the Messenger
  • Register a listener on the Messenger in the model for that particular message

True to my blog’s title, I will demonstrate this principle with an example. First, a message containing the result of an OpenFileDialog:

using System.Collections.Generic;
using System.IO;
using GalaSoft.MvvmLight.Messaging;

namespace LocalJoost.Behaviours
{
  public class FilesOpenedMessage : GenericMessage<IEnumerable<FileInfo>>
  {
    public FilesOpenedMessage(IEnumerable<FileInfo> parm)
      : base(parm)
    {
      Identifier = string.Empty;
    }

    public string Identifier{get;set;}
  }
}

Notice that the message also contains a property Identifier, acting like an optional additional message identifier. This is necessary, since the MVVMLight messenger seems to identify messages by type. Therefore, every method listening to a FilesOpenedMessage gets a message, and the various models would have no way to know if it was actually for them to act on it. Identifier makes coupling a particular listener to a particular sender possible. I’m sure there are other ways to get this done, I simply chose the simplest.

Getting on with the behavior:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
using GalaSoft.MvvmLight.Messaging;

namespace LocalJoost.Behaviours
{
  /// <summary>
  /// A behavior attaching to a button
  /// </summary>
  public class FileOpenBehavior : Behavior<Button>
  {
    // Properties - can be set from XAML
    public string MessageIdentifier { get; set; }
    public string Filter { get; set; }
    public bool MultiSelect { get; set; }

    protected override void OnAttached()
    {
      base.OnAttached();
      Filter = "All files (*.*)|*.*";
      AssociatedObject.Click += AssociatedObject_Click;
    }

    void AssociatedObject_Click(object sender, RoutedEventArgs e)
    {
      // Open the dialog and send the message
      var dialog = 
        new OpenFileDialog {Filter = Filter, Multiselect = MultiSelect};
      if (dialog.ShowDialog() == true)
      {
        Messenger.Default.Send(
          new FilesOpenedMessage(dialog.Files) 
          { Identifier = MessageIdentifier });
      }      
    }

    protected override void OnDetaching()
    {
      AssociatedObject.Click -= AssociatedObject_Click;
      base.OnDetaching();
    }
  }
}

Which works pretty simple: it attaches itself to the click event, composes a message and fires it away on the Messenger. In your model you register a listener like this:

public class DemoModel : ViewModelBase
{
  public DemoModel()
  {
    if (IsInDesignMode)
    {
      // Code runs in Blend --> create design time data.
    }
    else
    {
      Messenger.Default.Register<FilesOpenedMessage>(
        this,
        DoOpenFileCallback);
    }
  }

  private void DoOpenFileCallback(FilesOpenedMessage msg)
  {
    if (msg.Identifier != "1234") return;
    // Store result in SelectedFiles
    SelectedFiles = msg.Content.Select(f => f.Name).ToList();
  }
  // Rest of model omitted - see demo solution
}

The “Content” property of the message always contains the payload, i.e. whatever you had to pass to the message constructor – in this case, an IEnumerable<FileInfo>. In XAML you then bind the whole thing together like this, like any ordinary behavior:

<UserControl x:Class="DemoMessageBehaviour.MainPage"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:model="clr-namespace:DemoMessageBehaviour.Model;assembly=DemoMessageBehaviour.Model"
  xmlns:LJBehaviours="clr-namespace:LocalJoost.Behaviours;assembly=LocalJoost.Behaviours"
  xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"        
  mc:Ignorable="d"
  d:DesignHeight="300" d:DesignWidth="400">
  <UserControl.Resources>
    <model:DemoModel x:Key="MyDemoModel"/>
  </UserControl.Resources>
  <Grid x:Name="LayoutRoot" Background="White" DataContext="{StaticResource MyDemoModel}">
    <Grid.RowDefinitions>
      <RowDefinition Height="0.9*"/>
      <RowDefinition Height="0.1*"/>
    </Grid.RowDefinitions>
    <Button Content="Open" Grid.Row="1" IsEnabled="{Binding CanOpen}" >
      <i:Interaction.Behaviors>
        <LJBehaviours:FileOpenBehavior MessageIdentifier="1234" MultiSelect="True"/>
      </i:Interaction.Behaviors>
    </Button>
    <ListBox Grid.Row="0" ItemsSource="{Binding SelectedFiles}"/>
  </Grid>
</UserControl>

The Listbox is used to display the names of the selected files. I did leave this out, there’s enough code in this sample already. For those who want to have the full picture: a complete solution demonstrating the behavior can be found here.

Notice there’s an important difference using this technique in stead of the EventToCommand: when the user presses the button, the model cannot control the actual display of the OpenFileDialog: the click is handled by the behavior, not the model itself (via EventToCommand) and the OpenFileDialog is always displayed once the button is clicked. Therefore, the model should control if the user can press the button at all, which is done by binding the IsEnabled property to a property of the model (CanOpen – sorry, lame pun). Which is good UX practice anyway – controls that cannot be used should be disabled in stead of giving a ‘sorry, you can’t do that’ message whenever possible, but in this case it’s simply necessary.

I only showed how to do open a file from disk, but a pattern like this can be used to save files to disk as well, or do other things that might seem to require coding in the code behind file. That is no sin in itself – nobody from the MVVM police will show up at your doorstep and take your family away if you need to do that. At least that is what Laurent himself promised at his MIX10 talk ;-). But by coding a behavior, you enable designers to add fairly easily relatively complex actions using Blend without having to dive too deep into XAML. Using behaviors like this FileOpenBehavior makes it even easier to add interactivity than the EventToCommand (which was closed off anyway during security restrictions).

It can’t hurt to be nice to your designers – after all, they make the pretty stuff up front that makes your application sell ;-)

Oh, and one final thing: in all my previous posts I was talking about ‘behaviour’ in stead of ‘behavior’. I hope you all will excuse me for being educated in the Queen’s English, even tough I am just Dutch. ;-).

08 August 2010

A Windows Phone 7 multi touch pan/zoom behaviour for Multi Scale Images

Some may have read my foray into using Windows Phone 7 to view maps, utilizing a Multi Scale Image (msi), MVVM Light and some extension properties. This application works quite well, but being mainly a study in applied architecture, the user experience leaves much to be desired. Studying Laurent Bugnion’s Multi Touch Behaviour got me on the right track. Although Laurent’s behaviour is very good, it basically works by translating, resizing (and optionally rotating) the control(s) inside the FrameworkElement is is attached to. For various reasons this is not an ideal solution for a map viewer.

So I set out to make my own behaviour, the first one I ever made by the way, and it turned out to remarkably easy – less than 90 lines of code, including whitespace and comments:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interactivity;

namespace LocalJoost.Behaviours
{
  /// <summary>
  /// A behaviour for zooming and panning around on a MultiScaleImage
  /// using manipulation events
  /// </summary>
  public class PanZoomBehaviour : Behavior<MultiScaleImage>
  {
    /// <summary>
    /// Initialize the behavior
    /// </summary>
    protected override void OnAttached()
    {
      base.OnAttached();
      AssociatedObject.ManipulationStarted += AssociatedObject_ManipulationStarted;
      AssociatedObject.ManipulationDelta += AssociatedObject_ManipulationDelta;
    }
    /// <summary>
    /// Shortcut for the Multiscale image
    /// </summary>
    public MultiScaleImage Msi { get { return AssociatedObject; } }

    /// <summary>
    /// Screen point where the manipulation started
    /// </summary>
    private Point ManipulationOrigin { get; set; }

    /// <summary>
    /// Multiscale view point origin on the moment the manipulation started
    /// </summary>
    private Point MsiOrigin { get; set; }

    void AssociatedObject_ManipulationStarted(object sender,
ManipulationStartedEventArgs e) { // Save the current manipulation origin and MSI view point origin MsiOrigin = new Point(Msi.ViewportOrigin.X, Msi.ViewportOrigin.Y); ManipulationOrigin = e.ManipulationOrigin; } void AssociatedObject_ManipulationDelta(object sender, ManipulationDeltaEventArgs e) { if (e.DeltaManipulation.Scale.X == 0 && e.DeltaManipulation.Scale.Y == 0) { // No scaling took place (i.e. no multi touch) // 'Simply' calculate a new view point origin Msi.ViewportOrigin = new Point { X = MsiOrigin.X - (e.CumulativeManipulation.Translation.X / Msi.ActualWidth * Msi.ViewportWidth), Y = MsiOrigin.Y - (e.CumulativeManipulation.Translation.Y / Msi.ActualHeight * Msi.ViewportWidth), }; } else { // Multi touch - choose to interpretet this either as zoom or pinch var zoomscale = (e.DeltaManipulation.Scale.X + e.DeltaManipulation.Scale.Y) / 2; // Calculate a new 'logical point' - the MSI has its own 'coordinate system' var logicalPoint = Msi.ElementToLogicalPoint( new Point { X = ManipulationOrigin.X - e.CumulativeManipulation.Translation.X, Y = ManipulationOrigin.Y - e.CumulativeManipulation.Translation.Y } ); Msi.ZoomAboutLogicalPoint(zoomscale, logicalPoint.X, logicalPoint.Y); if (Msi.ViewportWidth > 1) Msi.ViewportWidth = 1; } } /// <summary> /// Occurs when detaching the behavior /// </summary> protected override void OnDetaching() { AssociatedObject.ManipulationStarted -= AssociatedObject_ManipulationStarted; AssociatedObject.ManipulationDelta -= AssociatedObject_ManipulationDelta; base.OnDetaching(); } } }

Method AssociatedObject_ManipulationStarted just records where the user started the manipulation, as well as what the MSI ViewportOrigin was on that moment. Method AssociatedObject_ManipulationDelta then simply checks if the delta event sports a scaling in x or y direction – if it does, it calculates the properties for a new ‘logical point’ to be fed into the ZoomAboutLogicalPoint method of the MultiScaleImage. If there is no scaling, the user just panned, and a new ViewportOrigin is being calculated in the MSI’s own coordinate system which runs from 0,0 to 1,1. And that’s all there is to it.

If you download my sample mapviewer application it’s actually quite simple to test drive this

  • Add a Windows Phone 7 class library LocalJoost.Behaviours to the projects
  • Reference this project from WP7viewer
  • Create the behaviour described above
  • Open MainPage.xaml in WP7Viewer
  • Find the MultiScaleImage called “msi” and remove all bindings except MapTileSource, so that only this remains:
<MultiScaleImage x:Name="msi" 
   MapMvvm:BindingHelpers.MapTileSource="{Binding CurrentTileSource.TileSource}">
</MultiScaleImage>
  • Add the behaviour to the MultiScaleImage using Blend or follow the manual procedure below:
  • Declare the namespace and the assembly in the phone:PhoneApplicationPage tag like this
xmlns:LJBehaviours="clr-namespace:LocalJoost.Behaviours;assembly=LocalJoost.Behaviours"
  • Add the behaviour to the MSI like this
<MultiScaleImage x:Name="msi" 
   MapMvvm:BindingHelpers.MapTileSource="{Binding CurrentTileSource.TileSource}">
  <i:Interaction.Behaviors>
      <LJBehaviours:PanZoomBehaviour/>
  </i:Interaction.Behaviors>
</MultiScaleImage>

And there you go. You can now zoom in and out using two or more fingers. That is, if you have a touch screen. If you don’t have a touch screen and you were not deemed important enough to be assigned a preview Windows Phone 7 device then you are in good company, for neither have I, and neither was I ;-). But fortunately there is Multi Touch Vista on CodePlex. It needs two mice (or more, but I don’t see how you can operate those), and it’s a bit cumbersome to set up, but at least I was able to test things properly. So, don’t let the lack of hardware deter you getting on the Windows Phone 7 bandwagon!

What I learned from this: behaviours will give an interesting twist to design decisions. When is it proper to solve things into behaviours, and when in a model by binding? For me, it’s clear that in this case the behaviour wins from the model – it’s far easier to make, understand and – above all – apply! For now, just drag and drop the behaviour on top of a MultiScaleImage and bang – zoom and pan. No complex binding expressions. 

Incidentally, although this behaviour was created with maps in mind, in can be applied to any MultiScaleImage of course, showing ‘ordinary’ image data.

For those who are not very fond of typing: the sample map application with the behaviour  already added and configured can be downloaded here. For those lucky b******s in possession of a real live device: you will find the XAP here. I would very much appreciate feedback on how the thing works in real life.