While working on Spotted I wanted to allow users to associate images with the places they want to remember. However, the view model that captured this image also has to be responsible for sending it across the wire on the save. I did not want to keep two properties, one to hold the raw data using byte[] and another that held the BitmapImage which represented the image to display. This article talks about how I got around this and still held true to MVVM.
My first attempt was to use simple inheritance to create a custom property in a custom Image class (I called it SpottedImage) which would feature my property would result in the fewest amount of changes. Example:
public class SpottedImage : Image { public byte[] ImageData { get { /* return the byte array from BitmapImage */ } set { /* convert the byte[] int BitmapImage */ } } }
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
This attempt did not work, mainly because, for whatever reason, Microsoft has chosen to “seal” many of the Windows Phone 7 control classes, making it impossible to extend. It perplexes me why they will allow us to modify the control templates to our hearts content, but we cant extend the controls to add a simple property.
Without being able to use simple inheritance the next choice was to wrap the Image control in a UserControl and provide a dependency property which could carry out the conversion. This worked beautifully, and with a bare minimum of code. First I had to define the user control, which is about as simple as you think:
<UserControl x:Class="Spotted.Controls.SpottedImage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Grid> <Image x:Name="TheImage" /> </Grid> </UserControl>
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Just a simple Grid containing the image, nothing special. The real magic is in the code-behind. Remember, since we are dealing with a “control” we don’t need to worry about binding within the control itself. The first step in the code behind is to define the dependency property we will bind to externally:
public static readonly DependencyProperty ImageDataProperty = DependencyProperty.Register("ImageData", typeof(byte[]), typeof(SpottedImage), new PropertyMetadata(ImageDataChanged));
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
This defines a simple static property which we can bind to. When the property changes, the method ImageDataChanged is fired giving us the ability to handle what the binding means. In this case, the incoming data type is byte[] but to assign as the Source of the Image we need to convert it to a BitmapImage. Here is the code for this method:
private static void ImageDataChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var control = (SpottedImage) d; using (var memStream = new MemoryStream((byte[])e.NewValue)) { var image = new BitmapImage(); image.SetSource(memStream); control.ImageSource = image; } }
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Very simple. Originally, this code was in the view model when I had two properties. But really, this isn’t data that the view model cares about and its not its responsibility here to convert this. This is on the control itself.
You notice that I set an ImageSource property after the conversion. This is purely internal and is never used outside the control. Here is the code for this property:
public BitmapImage ImageSource { get { return (BitmapImage)TheImage.Source; } private set { TheImage.Source = value; } }
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Pretty standard for a property. The set is only every used, but the get is required for purposes of assignment and control validation.
So this is all well and good, and you could use this right now, but you would be unable to take full advantage of the MVVM provided by Caliburn.Micro, which is convention based binding. This is, effectively, a new control which Caliburn would never know about by default. Luckily, CM allows you to create new conventions using ConventionManager within your bootstrapper. Here is my updated convention for SpottedImage:
ConventionManager.AddElementConvention( SpottedImage.ImageDataProperty, "ImageData", string.Empty);
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
One line. Yet it gives you the ability now to do something like this in your View:
<uc:SpottedImage x:Name="SpotPicture" HorizontalAlignment="Center" VerticalAlignment="Center" Stretch="Fill" />
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
And this in your View Model:
public byte[] SpotPicture { get; private set; }
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
And have it automatically do the binding for you. This is tremendously powerful, because now after the users takes a picture, I need only set this property and notify the framework that the property has changed and everything else is taken care of for me, like so:
public void TakePictureCompleted(object sender, PhotoResult photoResult) { // prepare the package for transport (convert to byte[]) using (var reader = new BinaryReader(photoResult.ChosenPhoto)) { long length = photoResult.ChosenPhoto.Length; byte[] imageData = reader.ReadBytes((int) length); SpotPicture = imageData; NotifyOfPropertyChange("SpotPicture"); } }
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Awesome. Clean and straightforward and in keeping with MVVM. This is why I love coding when you get to achieve things like this.