Part One: Starting with MVVM
Part Two: The MVVM solution structure and basic framework
Part Three: Base Classes
I want to go over the main view (SamplerView), view model (SamplerViewModel) and model (Sampler) classes next. I’ve gone over the ItemsControl that populates the TPL samples but I wanted to cover the other MVVM aspects as well as what isn’t MVVM here. There are two other primary MVVM components to this. One is the image chooser, the other is the log that is populated as the samples run. Remember that the DataContext of the SamplerView is a SamplerViewModel. This means that any binding is done on this object.
The Image Chooser:
Near the top of SamplerView is a TextBox that contains the path to the image that is used when a sample needs an image. Next to that is a Button that opens an OpenFileDialog to choose an image.
<Border Grid.Row="0" Grid.ColumnSpan="2" >
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Grid.Row="0" VerticalAlignment="Bottom" Text="{Binding Sampler.ImagePath, Mode=TwoWay}" />
<Button Grid.Column="1" Content="Change Image">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<ei:CallMethodAction TargetObject="{Binding}" MethodName="ChangeImagePath" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</Grid>
</Border>
Like the sample view model, the Sampler model is directly exposed in the SamplerViewModel. As such the binding of the TextBox is to the ImagePath property of the Sampler model. The mode is set to two-way so that if a user wishes they could just paste a path into the TextBox directly. What’s new here is the Interaction.Trigger. This allows you to define a method to call for the EventTrigger, in this case the “Click” event.
Here is the SamplerViewModel.ChangeImagePath():
public void ChangeImagePath()
{
OpenFileDialog ofd = new OpenFileDialog();
ofd.CheckFileExists = true;
ofd.CheckPathExists = true;
ofd.Multiselect = false;
if (ofd.ShowDialog() == true)
{
Sampler.ImagePath = ofd.FileName;
}
}
This has the standard OpenFileDialog code but when the user chooses a file, that path is set to the model’s ImagePath. Since the ImagePath is bound to the TextBox in the view, setting the path will update the TextBox to the correct path. Setting this image path has the side-effect that the images within the application get updated.
Sampler.ImagePath:
string imagePath = "";
public string ImagePath
{
get { return this.imagePath; }
set
{
if (this.imagePath != value)
{
this.ValidateProperty("ImagePath", value);
this.imagePath = value;
this.RaisePropertyChanged("ImagePath");
SetImageSources();
}
}
}
There are three important aspects to this.
One is the ValidateProperty:
protected override void ValidateProperty(string propertyName, object value)
{
if (propertyName == "ImagePath")
{
var errors = new List<string>();
string path = value as string;
if (string.IsNullOrEmpty(path))
{
errors.Add("Image path must be set.");
}
else if (!File.Exists(path))
{
errors.Add("The image indicated does not exist.");
}
this.ErrorsContainer.SetErrors(propertyName, errors);
}
else
{
base.ValidateProperty(propertyName, value);
}
}
When ValidateProperty is called it verifies that there is an image path and that the file exists. If either of these are not true than an error is added to the error container in the base domain object that allows the application to provide feedback to the user that there is an error.
Two is the RaisePropertyChanged event. This is pretty standard when binding as this notifies any observers that are bound to this object that the property has changed, that way they can change the value as they need to.
Finally is the Sampler.SetImageSources():
private void SetImageSources()
{
if(!File.Exists(ImagePath))
return;
BitmapImage sourceBitmapImage = new BitmapImage();
sourceBitmapImage.BeginInit();
sourceBitmapImage.UriSource = new Uri(ImagePath);
sourceBitmapImage.EndInit();
SourceImage = sourceBitmapImage;
ResetDestinationImage();
}
public void ResetDestinationImage()
{
if(!File.Exists(ImagePath))
return;
if (Application.Current.Dispatcher.Thread != Thread.CurrentThread)
{
Application.Current.Dispatcher.Invoke(() =>
{
ResetDestinationImage();
});
return;
}
BitmapImage destBitmapImage = new BitmapImage();
destBitmapImage.BeginInit();
destBitmapImage.UriSource = new Uri(ImagePath);
destBitmapImage.EndInit();
DestinationImage = destBitmapImage;
}
This is the side-effect of updating the image path. It causes the two images in the view, which are bound to the SourceImage and DestinationImage, to get updated. Since the ValidateProperty takes care of providing the user with the feedback that a file may not exists we don’t have to do any error handling beyond just making sure the image exists.
The last MVVM component of interest is the Submit button.
<Button Content="Submit" Height="23" IsEnabled="{Binding CanSubmit}" Margin="5" HorizontalAlignment="Right" Width="75">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<ei:CallMethodAction TargetObject="{Binding}" MethodName="Submit"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
Like the ChangeImage button above, this is bound to a method in the SamplerModelView.
SamplerModelView.Submit()
public async void Submit()
{
CurrentState = "Running";
await Sampler.RunSamples();
CurrentState = "Completed";
}
This method is run with async/await so the UI isn’t locked when the samples are run. I’ll discuss async/await in detail later in this series. It just runs the method to run the samples in the Sampler model. In the next post I’ll go more into depth on running samples.
Finally I want to discuss what is not MVVM here. The images are defined in SamplerView as:
<Border MinHeight="100" Grid.Row="1" Grid.Column="0" BorderBrush="Black" BorderThickness="2">
<Image ClipToBounds="True" Source="{Binding Sampler.SourceImage}">
<Image.RenderTransform>
<TransformGroup>
<ScaleTransform x:Name="imgScaleTransformSource" ScaleX="1" ScaleY="1" />
<TranslateTransform x:Name="imgTranslateTransformSource" />
</TransformGroup>
</Image.RenderTransform>
</Image>
</Border>
<Border MinHeight="100" Grid.Row="1" Grid.Column="1" BorderBrush="Black" BorderThickness="2">
<Image ClipToBounds="True" Source="{Binding Sampler.DestinationImage}">
<Image.RenderTransform>
<TransformGroup>
<ScaleTransform x:Name="imgScaleTransformDest" ScaleX="1" ScaleY="1" />
<TranslateTransform x:Name="imgTranslateTransformDest" />
</TransformGroup>
</Image.RenderTransform>
</Image>
</Border>
<StackPanel Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Bottom">
<RepeatButton x:Name="btnPlus" Width="25" Margin="5" Click="btnPlus_Click">+</RepeatButton>
<RepeatButton x:Name="btnMinus" Width="25" Margin="5" Click="btnMinus_Click">-</RepeatButton>
<RepeatButton x:Name="btnUp" Width="25" Margin="5" Click="btnUp_Click">?</RepeatButton>
<StackPanel Orientation="Horizontal">
<RepeatButton x:Name="btnLeft" Width="25" Margin="5" Click="btnLeft_Click">?</RepeatButton>
<RepeatButton x:Name="btnRight" Width="25" Margin="5" Click="btnRight_Click">?</RepeatButton>
</StackPanel>
<RepeatButton x:Name="btnDown" Width="25" Margin="5" Click="btnDown_Click">?</RepeatButton>
</StackPanel>
These are bound to the SourceImage and DestinationImage properties of the Sampler model, standard MVVM stuff. What’s not MVVM are the repeat buttons that move/zoom the images around. I opted for this approach, using regular code-behind, because the moving/zooming of the images doesn’t really have anything to do with our models. It doesn’t have anything to do with running samples or any data associated with the samples. Because of this it didn’t make sense to bind the buttons that move the images using an event trigger to methods in the SamplerViewModel. In this case it would have convoluted our ModelViews just for the sake of using MVVM. This is what I meant in the first post where I referred to this as a composite application. MVVM for the sake of MVVM is counter-intuitive and counter-productive. The purpose of our ModelViews and Models is to focus on running TPL samples and giving the results of those samples back to the user.
Up next is running samples, getting the data to the samples and getting the results back from the samples.
Thanks,
Brian