12 November 2010

Silverlight and Windows Phone 7 do not like DTDs

My current Windows Phone 7 project requires the possibility of reading configurations from various servers. For the GIS lovers among my audience: I am trying to read a GetCapabilities document from a WMS server. I was reading this one and this one, and the first one succeeded, while the second one failed. It became even more odd when I tried to write unit tests running from the full framework – then both links processed flawlessly. The error was “NotSupportedException” on XDocument.Load().

The second links contains a DTD from ye goode olde days and it turns out Silverlight and Windows Phone 7 by default are not very fond of DTD’s in documents. After some searching around I found the DtdProcessing Enumeration and I changed this code

var doc = XDocument.Load(stream)

into

using (var reader = XmlReader.Create(stream, 
  new XmlReaderSettings {DtdProcessing = DtdProcessing.Ignore}))
{
  var doc = XDocument.Load(reader)
}

And indeed, problem solved

02 November 2010

Swapping two elements in a bound ObservableCollection IMPROVED VERSION

Today I noticed something odd. I wanted to swap two elements in an ObservableCollection that was bound to a list box – so I could move objects up and down in the list box. I noticed that moving up always worked, and moving down led to a crash in my application with the message ‘The parameter is incorrect’ in the Application_UnhandledException method of App.Xaml. Fat lot of help that is, I can tell ya ;-)

Maybe this is a Windows Phone 7 only thing, but after a bit of experimenting I found out that you always have to change the value of the item that is last in the collection – i.e., the item with the highest index – first. This worked perfectly and I blogged about it. Later I found one exception, when I tried to swap the first and the second entry. Swapping second and first works, but the other way around gave once again ‘The parameter is incorrect’

This makes no sense at all, but I guess it has something to do with the fact that at one point in time the collection contains one object twice, and whatever is bound to it does not seem to like that very much. One of my household sayings is that were’s a will, there’s a workaround. So, second time around:

using System.Collections.Generic;
using System.Collections.ObjectModel;

using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace LocalJoost.Utilities
{
  public static class ObservableCollectionExtensions
  {
    public static void Swap<T>(
       this ObservableCollection<T> collection, 
       T obj1, T obj2)
    {  
      if (!(collection.Contains(obj1) && collection.Contains(obj2))) return;
      var indexes = new List<int>
         {collection.IndexOf(obj1), collection.IndexOf(obj2)};
      if(indexes[0] == indexes[1]) return;
      indexes.Sort();
      var values = new List<T> {collection[indexes[0]], collection[indexes[1]]};
      collection.RemoveAt(indexes[1]);
      collection.RemoveAt(indexes[0]);
      collection.Insert(indexes[0], values[1]);
      collection.Insert(indexes[1], values[0]);
    }
  }
}
and now I am really done with it. It's not always high science what I blog, just things I hope that save someone else a lot of time :-)

21 October 2010

Showing Open Source maps on Windows Phone 7 with the Bing Maps Control

My previous post, which caused a bit of a stirrup and got me over 1000 page hits on that one page alone in less than 46 hours, led to the question if I could provide an example of how to use data from Open Source map servers on the web in Bing Maps. This is more useful than Google Maps, since Google’s TOS most likely does not allow accessing their data they way I showed, and an application doing so violates section 3.1 of the Windows Phone 7 Application Certification Requirements, which clearly state that allowed content includes “Copyrighted content that is used with permission”, which I obviously do not have and most likely never will. Unless Google wants to code me a Windows Phone 7 app for them. Hello? Anyone reading along there at Mountain View? ;-)

So anyway, as long as my personal Google Maps for Windows Phone 7 is not going to hit the Marketplace, the Bing Maps MercatorMode can very well be used to show other tile maps, and by popular request I show you how to use data from Mapnik and Osmarender:

MapnikOsmarender

The possibility should come as no surprise, since I showed something like this earlier, only then with a plain ole’ MultiScaleImage. All you need to do is, once again, write a class again that descends from Microsoft.Phone.Controls.Maps.TileSource and overrides

Uri GetUri(int x, int y, int zoomLevel)

like I showed in my previous post. And these are even simpler than the Google Maps tilesource. First, Mapnik:

using System;

namespace LocalJoost.TileSource
{
  public class MapnikTileSource : Microsoft.Phone.Controls.Maps.TileSource
  {
    public MapnikTileSource()
    {
      UriFormat = "http://{0}.tile.openstreetmap.org/{1}/{2}/{3}.png";
      _rand = new Random();
    }

    private readonly Random _rand;
    private readonly static string[] TilePathPrefixes =
        new[] { "a", "b", "c" };

    private string Server
    {
      get
      {
        return TilePathPrefixes[_rand.Next(3)];
      }
    }
   
    public override Uri GetUri(int x, int y, int zoomLevel)
    {
      if (zoomLevel > 0)
      {
        var url = string.Format(UriFormat, Server, zoomLevel, x, y);
        return new Uri(url);
      }
      return null;
    }
  }
}

and second, Osmarender

using System;

namespace LocalJoost.TileSource
{
  public class OsmaRenderTileSource : Microsoft.Phone.Controls.Maps.TileSource
  {
    public OsmaRenderTileSource()
    {
      UriFormat = "http://{0}.tah.openstreetmap.org/Tiles/tile/{1}/{2}/{3}.png";
      _rand = new Random();
    }

    private readonly Random _rand;
    private readonly static string[] TilePathPrefixes =
        new[] { "a", "b", "c", "d", "e", "f" };

    private string Server
    {
      get
      {
        return TilePathPrefixes[_rand.Next(6)];
       }
    }

    public override Uri GetUri(int x, int y, int zoomLevel)
    {
      if (zoomLevel > 0)
      {
        var url = string.Format(UriFormat, Server, zoomLevel, x, y);
        return new Uri(url);
      }
      return null;
    }
  }
}

As you see, no rocket science. How to use this classes in XAML is explained in my previous post, and I am not going to repeat that here..

The odd thing is that the Bing Maps logo stays visible, although no Bing Maps data is being used. Notice that the open source maps are displayed in design mode too. So, use any tile server out there that you can legally access and have fun mapping…. although I must admit that is most likely interesting for hardcore GIS fans only, since both servers are considerably slower than Bing – especially OsmaRender, which does some odd things cutting the Netherlands in half, as you see. But still, if not anything else, it shows off the power and versatility of the Bing Maps control pretty well.

For the lazy people, a complete solution – including the Google stuff – can be downloaded here.

19 October 2010

Google Maps for Windows Phone 7 using the Bing Maps Control

GoogleMapsAs I write this, we are less than 25 hours away from the moment the first Windows Phone 7 devices are released into the wild – in the Netherlands, that is. The marketplace is filling up nicely already. Some things are pretty obviously missing. And you wonder why, because they would be quite easy to make.

I hereby dare Google to publish a Google Maps application in the Windows Phone 7 Marketplace. Why? Because if they don’t do it, someone else probably will, and probably pretty fast, too. See the screenshot to the left. I assure you – this is no fake. What you see here is the Google Maps satellite layer with the Street layer on top of it, with a 40% opacity, showing a piece of the city center of Amersfoort, Netherlands. It took me about 90 lines of code and 18 lines of XAML.

Setting up a basic Bing Map control is easy as cake. You find the grid “Contentpanel”, and you plonk a Map in it, preferably using Blend:

 

 

 

 

 

 

 

<Microsoft_Phone_Controls_Maps:Map 
 x:Name="map"  
 CredentialsProvider="your_credentials" Mode="Road">
</Microsoft_Phone_Controls_Maps:Map>

You can change “Road” into “Aerial” and then you have Bing Satellite imagery. What’s less known is that you can actually attach your own tile layers to it. A tile is an image of 256x256 defined by it’s X and Y position in the grid forming the map of the world on a specific zoom level. All you have to to is write a little class that descends from Microsoft.Phone.Controls.Maps.TileSource in which you only have to override the following method:

Uri GetUri(int x, int y, int zoomLevel)

in which you tell which x/y/zoomlevel combination translates to which URI. For Google Maps, this turns out to be pretty easy. All information needed for can be found in the Deep Earth source code – a wee bit adapted for for the Bing Map Control. First, I made an enum that defines all the layers that Google provides

namespace LocalJoost.TileSource
{
  public enum GoogleTileSourceType
  {
    Street,
    Hybrid,
    Satellite,
    Physical,
    PhysicalHybrid,
    StreetOverlay,
    WaterOverlay
  }
}

Then, the actual tile calculating class:

using System;

namespace LocalJoost.TileSource
{
  public class GoogleTileSource : Microsoft.Phone.Controls.Maps.TileSource
  {
    public GoogleTileSource()
    {
      UriFormat = @"http://mt{0}.google.com/vt/lyrs={1}&z={2}&x={3}&y={4}";
      TileSourceType = GoogleTileSourceType.Street;
    }
    private int _servernr;
    private char _mapMode;

    private int Server
    {
      get
      {
        return _servernr = (_servernr + 1) % 4;
      }
    }

    private GoogleTileSourceType _tileSourceType;
    public GoogleTileSourceType TileSourceType
    {
      get { return _tileSourceType; }
      set
      {
        _tileSourceType = value;
        _mapMode = TypeToMapMode(value);
      }
    }

    public override Uri GetUri(int x, int y, int zoomLevel)
    {
      {
         if (zoomLevel > 0)
        {
          var url = string.Format(UriFormat, Server, _mapMode, zoomLevel, x, y);
          return new Uri(url);
        }
      }
      return null;
    }

    private static char TypeToMapMode(GoogleTileSourceType tileSourceType)
    {
      switch (tileSourceType)
      {
        case GoogleTileSourceType.Hybrid:
          return 'y';
        case GoogleTileSourceType.Satellite:
          return 's';
        case GoogleTileSourceType.Street:
          return 'm';
        case GoogleTileSourceType.Physical:
          return 't';
        case GoogleTileSourceType.PhysicalHybrid:
          return 'p';
        case GoogleTileSourceType.StreetOverlay:
          return 'h';
        case GoogleTileSourceType.WaterOverlay:
          return 'r';
      } return ' ';
    }
  }
}

As you can see, this is not quite rocket science. To make the Bing Maps control use this, you need to expand your XAML a little. First, you have to add two namespaces to your MainPage.Xaml:

xmlns:MSPCMCore="clr-namespace:Microsoft.Phone.Controls.Maps.Core;assembly=Microsoft.Phone.Controls.Maps" 
xmlns:LJTileSources="clr-namespace:LocalJoost.TileSource;assembly=LocalJoost.TileSource" 

Then, you have to use a third mode for the Bing Map – Mercator. This basically only tells the map to operate in Mercator mode, but not to attach any default imagery. And here we go:

<Microsoft_Phone_Controls_Maps:Map x:Name="map" 
   CredentialsProvider="your credentials" >
  <Microsoft_Phone_Controls_Maps:Map.Mode>
    <MSPCMCore:MercatorMode></MSPCMCore:MercatorMode>
  </Microsoft_Phone_Controls_Maps:Map.Mode>
  <Microsoft_Phone_Controls_Maps:Map.Children>
    <Microsoft_Phone_Controls_Maps:MapTileLayer>
      <Microsoft_Phone_Controls_Maps:MapTileLayer.TileSources>
        <LJTileSources:GoogleTileSource TileSourceType="Satellite"/>  
      </Microsoft_Phone_Controls_Maps:MapTileLayer.TileSources>
    </Microsoft_Phone_Controls_Maps:MapTileLayer>
  </Microsoft_Phone_Controls_Maps:Map.Children>
</Microsoft_Phone_Controls_Maps:Map>
Presto. Google Maps for Windows Phone 7. And oh, if you want to project another layer on top of it, for instance Google Streets, like I did, you just add more tilelayers:
<Microsoft_Phone_Controls_Maps:Map x:Name="map" 
   CredentialsProvider="your credentials" >
<Microsoft_Phone_Controls_Maps:Map.Mode>
  <MSPCMCore:MercatorMode></MSPCMCore:MercatorMode>
</Microsoft_Phone_Controls_Maps:Map.Mode>
<Microsoft_Phone_Controls_Maps:Map.Children>
  <Microsoft_Phone_Controls_Maps:MapTileLayer>
    <Microsoft_Phone_Controls_Maps:MapTileLayer.TileSources>
      <LJTileSources:GoogleTileSource TileSourceType="Satellite"/>
    </Microsoft_Phone_Controls_Maps:MapTileLayer.TileSources>
  </Microsoft_Phone_Controls_Maps:MapTileLayer>
  <Microsoft_Phone_Controls_Maps:MapTileLayer>
    <Microsoft_Phone_Controls_Maps:MapTileLayer.TileSources>
      <LJTileSources:GoogleTileSource TileSourceType="Street"/>
    </Microsoft_Phone_Controls_Maps:MapTileLayer.TileSources>
  </Microsoft_Phone_Controls_Maps:MapTileLayer>
</Microsoft_Phone_Controls_Maps:Map.Children>
</Microsoft_Phone_Controls_Maps:Map>

The Bing Maps control makes you able to zoom and pan trough all this. Okay, Google Maps does a little more than this, but this shows pretty well how darn easy it is to make a good looking, pretty fully functional mapping application showing totally different imagery.

Conclusion: using the Bing Maps Control for showing raster maps is so bloody easy that I almost start to wonder if it’s still justified to feel proud ‘being a GIS guy’. Almost ;-)

17 October 2010

Browse the Marketplace for Windows Phone 7 apps outside the USA

This article does technically not belong to this blog, for it does not contain any code samples – in fact it does not handle code at all. But since the launch of Windows Phone 7 is a very special occasion and the devices will hit my country (the Netherlands) very soon I thought it important enough to break rank for once.

For all kinds of reasons – mostly legal, I’ve been told - about 17 countries are included in the Marketplace for Windows Phone 7. This by no means that you cannot be part of the fun of the Windows Phone 7 launch if you are not inside the USA or any of the other countries – like me. All you have to do to view the Windows Phone 7 apps that are currently in the Marketplace is follow the simple procedure below:

  1. Install Zune software (if you haven’t done that already)
  2. Browse to www.msn.com. This will redirect you to your local msn (in my case, nl.msn.com) but will also provide you with a popup that lets you choose between your local msn.com and the USA msn.com. Choose the USA version
  3. If you are signed in with your live ID, sign out
  4. Create a new live ID that has its location in the USA. You need to provide an USA state and ZIP code that actually exist and match. Which can be easily obtained via this site. Simply click somewhere on the map, choose one of the cities it suggests, and presto – one existing zip code and state combination
  5. Start up the Zune software
  6. Login with your freshly created live ID
  7. Provide your birth date
  8. Click Marketplace
  9. Click Apps
  10. And there you are

Update according to Rob Houweling this procedure will only work if the OS language is set to English. See comment. Thanks Rob!

Update 2 this works on a Windows Phone 7 as well. Tom Verhoeff has a blog post which explains (in Dutch) how you can setup a phone to use a specific Live Id that has it's location in the UK and a billing adress in the UK that will allow you to buy apps using an ordinary Dutch credit card.

Bear in mind that your local Microsoft office does not support this procedure, so when you have problems you are basically on your own. But that is what you have your local Microsoft community for - they can probably help you out, and if not, there are plenty of Microsoft employees who will help you out sub rosa – if you just ask the right person kindly enough. After all, we’re all in the same boat ;-)

Have fun!

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. ;-).

30 September 2010

Notifying the viewmodel of input errors using MVVMLight in Silverlight

I was creating a little application that had some input fields and a submit button. I wanted the submit button only to be enabled when the user input was valid. No a very unusual use case. So I set out to create a viewmodel the usual MVVMLight way, extending ViewModelBase and making properties using the snippets Laurent Bugnion provides with the MVVMLight toolkit. To validate them, I decorated them with attributes from System.ComponentModel.DataAnnotations, and validated them with using the Validator. A typical property looks like this:

/// 
/// The  property's name.
/// 
public const string SomecodePropertyName = "Somecode";

private int _somecode = 0;

[Range(0, 2000)]
public int Somecode
{
  get
  {
    return _someCode;
  }

  set
  {
    if (_someCode == value)
    {
      return;
    }
    Validator.ValidateProperty(value,
      new ValidationContext(this, null, null) 
      { MemberName = SomecodePropertyName });

    _someCode = value;

    // Update bindings, no broadcast
    RaisePropertyChanged(SomecodePropertyName);
  }
}

Then I thought I was smart, so I added the following code:

private void RaisePropertyChangedAndRevalidate(string propertyName)
{
  RaisePropertyChanged(propertyName);
  RaisePropertyChanged("IsValid");
}

public bool IsValid
{
  get
  {
    return Validator.TryValidateObject(this, 
                       new ValidationContext(this, null, null),
                       new List<ValidationResult>());
  }
}

and changed every other call to RaisePropertyChanged to RaisePropertyChangedAndRevalidate. Then I bound my button’s IsEnabled property to IsValid

<Button Content="subMit" x:Name="btnSave" 
  IsEnabled="{Binding IsValid,ValidatesOnExceptions=True,NotifyOnValidationError=True}">

and I turned out not be so very smart after all, for it did not work. When the user entered a value that was outside the range of 0-2000 or even plain nonsense – like “thisisnonumber”, the UI gave an error message, I even got the message in my ValidationSummary, but for some reason the viewmodel still found itself valid.

This puzzled me for a while, but it turned out to be perfectly logical. If the user inputs nonsense this throws an exception, and so does a call to Validator.ValidateProperty. So the invalid input never makes to the viewmodel property, and the viewmodel, if it was initially valid, stays valid en thus my button stays enabled. How to get around this?

Actually, it’s pretty simple: notify your viewmodel of input errors, so it can keep track of whatever nonsense the user has tried to cram down its throat. The way I chose to do it was like this:

  • Add a field int _fieldErrors;
  • Bind the event BindingValidationError to a command in the viewmodel, passing it’s parameters along
  • Add to or subtract 1 from_fieldErrors for every time the command was called depending on the parameter
  • Modify the IsValid property so it checks the _fieldErrors field to.

In code:

private void RaisePropertyChangedAndRevalidate(string propertyName)
{
  RaisePropertyChanged(propertyName);
  if (_fieldBindingErrors == 0)
  {
    RaisePropertyChanged("IsValid");
  }           
}
private int _fieldBindingErrors = 0;

public ICommand RegisterFieldBindingErrors
{
  get
  {
    return new RelayCommand<ValidationErrorEventArgs>(e =>
    {
      if (e.Action == ValidationErrorEventAction.Added)
      {
        _fieldBindingErrors++;
      }
      else
      {
        _fieldBindingErrors--;
      }
      RaisePropertyChanged("IsValid");
    });
  }
}

public bool IsValid
{
  get
  {
    return Validator.TryValidateObject(this, 
                       new ValidationContext(this, null, null),
                       new List<ValidationResult>()) 
                       && _fieldBindingErrors == 0;
  }
}

Binding the stuff in XAML goes like this:

<StackPanel
  DataContext="{Binding MyModel,Mode=TwoWay,ValidatesOnExceptions=True,NotifyOnValidationError=True}">
  <i:Interaction.Triggers>
    <i:EventTrigger EventName="BindingValidationError">
          <cmd:EventToCommand Command="{Binding RegisterFieldBindingErrors}" 
            PassEventArgsToCommand="true"/>
        </i:EventTrigger>
      </i:Interaction.Triggers>
</StackPanel>

And thus the viewmodel maintains its own integrity, is notified by the UI of all problems playing around there, and can decide based upon the sum of this information whether or not the user can actually save.

Thanks to Mike Taulty for helping me out on the Validator in real-time on Twitter.