What is the WPF answer to this? - wpf

I have used WPF to develop two moderately sized applications. I was much impressed by the cleanness of WPF and its features. When I explained to one of my colleagues (Who happens to develop business apps) the various benefits of WPF, he challenged me with this problem which had me totally stumped:
The Problem:
He coded an application in the following way in about 2 minutes:
Open a new WinForms project.
Define a class Loan.
Build project.
Define an object data source using Loan.
In Data Sources explorer, change view type of the Loan data source to Details.
Drag the data source onto the Form in the designer.
Supply the data source with a Loan[] containing one object.
Build and run application.
The code:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace WinForms_DataBinding_Example
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
loanBindingSource.DataSource = new Loan[] { new Loan() };
}
}
public class Loan
{
public decimal Amount { get; set; }
public decimal Rate { get; set; }
public decimal Total { get { return Amount * Rate; } }
}
}
The designer:
The application:
Now whenever you change the value of Amount or Rate in the window, the value of Total changes accordingly. After explaining that this is a very useful feature in business apps where any changes you make to one property in an entity immediately updates the view where calculated properties are refreshed instantly making the user experience better. Considering that the typical business entity class has a lot of properties, this saves a lot of coding. Then he asked me to do the same in WPF.
I first explained to him that I do not understand what sort of black magic goes on here. How does the Total textbox update itself automatically? This is my first question:
Q1. The Loan class does not implement INotifyPropertyChanged or something similar. So how does the Total textbox get updated when the Amount or Rate textboxes lose focus?
Then I told him that I do not know how to do the same thing so easily in WPF. However, I wrote the same app in WPF with 3 TextBlocks and 3 TextBoxs in the UI. I also needed to make Loan class implement INotifyPropertyChanged. Added backing fields to Amount and Rate. Whenever these properties were being set, I raised a property changed notification for the property Total. In the end, I was left with an app with badly aligned controls which did the same thing as the WinForms app. However, this was way harder to do than the WinForms method.
I came home and then had the bright idea of drag-dropping the Loan data source on to the WPF window (After I changed the view mode to detail). Sure enough, I got the same kind of UI as in WinForms app and after setting the data source to the same Loan[] as in WinForms app, it seemed to be complete. I ran the app, changed the Amount and Rate fields hoping to see Total change itself automagically. However, I was disappointed. The Total field did not change:
The code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using WinForms_DataBinding_Example;
namespace WPF_Grid_Example
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Window_Loaded_1(object sender, RoutedEventArgs e)
{
System.Windows.Data.CollectionViewSource loanViewSource = ((System.Windows.Data.CollectionViewSource)(this.FindResource("loanViewSource")));
// Load data by setting the CollectionViewSource.Source property:
loanViewSource.Source = new List<Loan>() { new Loan() };
}
}
}
The xaml:
<Window
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:WinForms_DataBinding_Example="clr-namespace:WinForms_DataBinding_Example;assembly=WinForms_DataBinding_Example" mc:Ignorable="d" x:Class="WPF_Grid_Example.MainWindow"
Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded_1">
<Window.Resources>
<CollectionViewSource x:Key="loanViewSource" d:DesignSource="{d:DesignInstance {x:Type WinForms_DataBinding_Example:Loan}, CreateList=True}"/>
</Window.Resources>
<Grid>
<Grid x:Name="grid1" DataContext="{StaticResource loanViewSource}" HorizontalAlignment="Left" Margin="121,123,0,0" VerticalAlignment="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Content="Amount:" Grid.Column="0" HorizontalAlignment="Left" Margin="3" Grid.Row="0" VerticalAlignment="Center"/>
<TextBox x:Name="amountTextBox" Grid.Column="1" HorizontalAlignment="Left" Height="23" Margin="3" Grid.Row="0" Text="{Binding Amount, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}" VerticalAlignment="Center" Width="120"/>
<Label Content="Rate:" Grid.Column="0" HorizontalAlignment="Left" Margin="3" Grid.Row="1" VerticalAlignment="Center"/>
<TextBox x:Name="rateTextBox" Grid.Column="1" HorizontalAlignment="Left" Height="23" Margin="3" Grid.Row="1" Text="{Binding Rate, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}" VerticalAlignment="Center" Width="120"/>
<Label Content="Total:" Grid.Column="0" HorizontalAlignment="Left" Margin="3" Grid.Row="2" VerticalAlignment="Center"/>
<TextBox x:Name="totalTextBox" Grid.Column="1" HorizontalAlignment="Left" Height="23" Margin="3" Grid.Row="2" Text="{Binding Total, Mode=OneWay}" VerticalAlignment="Center" Width="120"/>
</Grid>
</Grid>
</Window>
Q2. I was confounded before by the black magic of WinForms, I was confounded now because the same black magic did not work in WPF. Why?
Q3. How do I make the WPF version to update the Total field automatically as in the WinForms example?
Q4. Which platform is better/faster for this sort of business app development? If I am to make a better argument on behalf of WPF, what should I be looking at?
I hope I was clear about the problem. Please let me know if any clarifications are needed. Thanks.

Q1: If you look at the designer file for the Windows Form you'll see about 300 lines of code generated for your 3 textboxes. Some of this code is similar to:
this.amountTextBox.DataBindings.Add(
new System.Windows.Forms.Binding("Text",
this.loanBindingSource, "Amount", true));
The Binding and the BindingSource co-operate to update the bound values and cause all bound controls to be updated every time one of the values changes (using reflection).
Q2: Because the WPF designer doesn't create a .Designer.cs file and the associated mess of code. You need to explicitly implement INotifyPropertyChange, which can be simplified by using say MVVM Light's ViewModelBase, e.g.
public class Loan : ViewModelBase
{
public decimal Amount
{
get
{
return this.amount;
}
set
{
if (Set(() => Amount, ref this.amount, value))
{
RaisePropertyChanged(() => Total);
}
}
}
Q3:
1) When Amount or Rate changes raise the property change notification for that property but also for the computed property 'Total'.
2) Modify your bindings on Amount and Rate to Binding="{Binding Amount, UpdateSourceTrigger=LostFocus}"
Q4: WPF no question (IMHO). The WPF way is more testable and maintainable and understandable.

Answer to Q4:
Regardless of winforms having the ability to generate 3 silly textboxes for a class, WPF is a much better, scalable and powerful framework. It has much greater performance due to hardware acceleration and whatnot, and requires less or no code to do some tasks that take tons of code in winforms, such as this, or this:
<CheckBox x:Name="chk"/>
<TextBox IsEnabled="{Binding IsChecked,ElementName=chk}"/>
Also, tipical Line-of-Business applications have to deal with thousands or hundreds of thousands of records, and UI Virtualization makes a huge difference.
The bottom line is that winforms, regardless of having some designer goodies (which are more a feature of Visual Studio than winforms itself), is nowhere near as practical and adequate when it comes to Line of Business.

Related

AvalonDock Now Loses Alt Key Adornments

I've been using AvalonDock (2.0) for some time now, being key for managing documents in an IDE. In the last month or so, I've noticed that Alt key adornments are no longer showing up for controls within AvalonDock, though the Alt key commands are executing as expected. See the image below where the Alt key adornments are showing up in the menu, but not for the buttons inside AvalonDock:
What is particularly interesting about this issue, that it appears to be triggered by an environmental setting or condition.
As demonstrated in this video (at 2:07), the alt key adornments are working for a control within AvalonDock. But, if I now use the very same executable I used in that video, the alt key adornments do not work.
I'm currently using build 2.0.1746, but I also tried build 2.0.2000 (with the Xceed namespace) and found the same issue. I also tried the version packaged with the Xceed Extended WPF Toolkit, and found that the issue persists.
I also built a very simple test application which loads a couple of documents of type Item, where Item is a simple class with a Name property:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
xmlns:avalonDock="clr-namespace:Xceed.Wpf.AvalonDock;assembly=Xceed.Wpf.AvalonDock"
xmlns:avalonDockLayout="clr-namespace:Xceed.Wpf.AvalonDock.Layout;assembly=Xceed.Wpf.AvalonDock"
xmlns:avalonDockControls="clr-namespace:Xceed.Wpf.AvalonDock.Controls;assembly=Xceed.Wpf.AvalonDock"
Title="MainWindow" Height="500" Width="500">
<Window.Resources>
<ResourceDictionary>
<DataTemplate DataType="{x:Type local:Item}">
<StackPanel Orientation="Vertical">
<Label Content="Item:" />
<TextBox Text="{Binding Name}"/>
<Button Content="_ClickMe" />
</StackPanel>
</DataTemplate>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<avalonDock:DockingManager Grid.Row="0" DocumentsSource="{Binding}">
<avalonDockLayout:LayoutRoot>
<avalonDockLayout:LayoutPanel Orientation="Horizontal">
<avalonDockLayout:LayoutDocumentPane/>
</avalonDockLayout:LayoutPanel>
</avalonDockLayout:LayoutRoot>
</avalonDock:DockingManager>
<Button Grid.Row="1" Content="_Test" />
</Grid>
</Window>
Even with this simple application, the Alt key adornments show up for the Test button outside of AvalonDock, but not for the ClickMe buttons for the controls within AvalonDock.
I also posted the issue here on the AvalonDock codeplex site, but there appears to be very little response activity. I also posted the issue here on the Extended Toolkit codeplex site.
Any ideas on how to correct or workaround this issue?
It sounds as though this will be fixed with the next version of AvalonDock.
In the meantime, the following Blend behavior is a workaround:
public class FixKeyboardCuesBehavior : Behavior<UIElement>
{
private static readonly DependencyProperty ShowKeyboardCuesProperty;
static FixKeyboardCuesBehavior()
{
Type keyboardNavigation = typeof(KeyboardNavigation);
var field = keyboardNavigation.GetField("ShowKeyboardCuesProperty", BindingFlags.NonPublic | BindingFlags.Static);
Debug.Assert(field != null, "field != null");
ShowKeyboardCuesProperty = (DependencyProperty)field.GetValue(null);
}
protected override void OnAttached()
{
base.OnAttached();
Window rootWindow = Window.GetWindow(this.AssociatedObject);
if (rootWindow == null)
{
return;
}
BindingOperations.SetBinding(
this.AssociatedObject,
ShowKeyboardCuesProperty,
new Binding("(KeyboardNavigation.ShowKeyboardCues)") { Source = rootWindow });
}
}
Use this from XAML by adding the following to the root element of your DataTemplate for the AvalonDock LayoutItemTemplate:
<i:Interaction.Behaviors
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity">
<my:FixKeyboardCuesBehavior />
</i:Interaction.Behaviors>
This workaround uses internal implementation details of WPF to re-introduce the Alt behaviour below the broken AvalonDock logical tree. So, I'll be looking forward to being able to zap it from my code when AD itself is fixed!

Combobox Dropdown event triggers parent window Z index to change

I have a problem with a WPF business line application prototype that I do not know where to start to debug.
My Master Window (A) opens a "dialog" window using Dialog.Show(). The child window (B) has a combobox on it.
When the combobox is clicked (to expand and to show all the options) the entire second window (B) is hidden except the dropdownlist appearing from the combobox. The combobox isn't there, the window isn't there. Nothing is there except the dropdownlist and the master window behind it. If I click the master window then focus is once again set to window B and it shows as it should (because I set the Owner of window B to be window A).
To make it more interesting this bug is not consistent. It appears maybe 1 of 20 times I use the application, and when it starts appearing it can appear several times in a row and then stop happening again.
Possibly related is the fact that I think I have the same bug some times with MessageBoxes. When using MessageBox.Show() (Win forms msgbox) from a viewmodel when only the master window (A) is showing the box occationally appears behind the master window making it invisible for the user.
My application is using GalaSoft MvvmLight (if that could have anything to do with it) and quite a few telerik components. Other than that I am not sure what data to provide. I don't think anyone can tell me the source of the problem based on this information, but where would you start looking for the problem?
Update :
Good news! I have isolated the problem and found the combination of prerequisites for the phenomena to occur:
a) A component showing a PDF file is currently open anywhere withing the application.
b) A telerik component has been undocked and redocked.
I will include the code below, but I think the issue is buried deep within the RadDocking or WebBrowser component.
MainWindow
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void OpenWindowClick(object sender, RoutedEventArgs e)
{
var w = new TestWindow { Owner = this, DataContext = new TestViewModel()};
w.Show();
}
}
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:telerikDocking="clr-namespace:Telerik.Windows.Controls;assembly=Telerik.Windows.Controls.Docking"
x:Class="TelerikGridDockingVsBrowserVsWindowBug.MainWindow"
Title="MainWindow" Height="750" Width="925">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="200"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<telerikDocking:RadDocking Grid.Row="0">
<telerikDocking:RadSplitContainer>
<telerikDocking:RadPaneGroup>
<telerikDocking:RadPane Header="I make bugs combined with a webbrowser showing a pdf document">
<TextBlock Text="1. Drag me and dock on the same or another location"/>
</telerikDocking:RadPane>
</telerikDocking:RadPaneGroup>
</telerikDocking:RadSplitContainer>
</telerikDocking:RadDocking>
<Button Grid.Row="1" Click="OpenWindowClick" Content="2. Open window" Height="50" Margin="0,20"/>
<WebBrowser Grid.Row="2" Source="http://www.kb.nl/sites/default/files/docs/pdf_guidelines.pdf"/>
</Grid>
TestWindow :
public partial class TestWindow : Window
{
public TestWindow()
{
InitializeComponent();
}
}
<Window x:Class="TelerikGridDockingVsBrowserVsWindowBug.TestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="TestWindow" Height="150" Width="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock> 3. Click on the combobox.</TextBlock>
<ComboBox Grid.Row="1" ItemsSource="{Binding Options}" SelectedItem="{Binding SelectedOption}" Height="50"></ComboBox>
<TextBlock Grid.Row="2"> This window will then disappear behind its owner</TextBlock>
</Grid>
TestWindow ViewModel:
public class TestViewModel
{
public List<string> Options { get; set; }
public string SelectedOption { get; set; }
public TestViewModel()
{
Options = new List<string> { "String1", "String2" };
SelectedOption = Options.First();
}
}
I have an open ticket with Telerik about this, so I will update here if I find out anything.
I have been in your situation before... you have an unexplained problem... it doesn't make any sense... you've tried everything that you can think of... in a last ditch effort to fix the problem, you throw it out to all the experienced users here... then nothing... no responses... no help.
The thing is that if you can't find and fix the problem with all of your code right in front of you, then how can we fix it without any code?
All I can suggest is for you to try to locate exactly where the problem originates. You can do this by commenting out sections of code and then running your solution to see if that has removed the problem or whether it still exists. The fact that your problem only occurs infrequently will make this stage even more tricky.
However, with perseverance, you should be able to narrow down where the problem arises. Once you have managed to exclude enough of your code, the last step is to see if you re-create it in a much smaller new WPF project. If you manage to get to this stage, please come back and show your reduced code... then, we may be able to help you further.

WPF MVVM Implementation

I have just started using WPF with MVVM pattern. I had gone through some material related to MVVM.
However, the project I have to work on has an implementation of MVVM that seems very different than what I have read (maybe incorrect as well, not sure).
The implementation has all the Views (controls or windows) implemented as ResourceDictionary where all the controls, in the view are in the "Style" element.
The code behind for such ResourceDictionary have all the DependencyProperty and the Commands (there is no other class for ViewModel). Also, the classes (code behind) some how inherit from the Windows.Controls.Control class.
Is this the correct implementation ? If not what are the reasons that you see that prove this as a wrong implementation.
I may be wrong but the reasons I see are the following:
Implementing views as ResourceDictionary is not correct and Resources are not for creating custom views.
Having minimal code in the code behind is one of the important aspects of MVVM, that allows for loosely coupled architecture.
Since all views inherit from Windows.Controls.Control, writing unit test cases for the views would be difficult.
Am I correct or there are some other reasons that this implementation is incorrect (or am I wrong and this can be a way to implement MVVM in WPF).
Your views are highly appreciated.
Below is a sample code: (XAML)
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Presentation"
>
<Style TargetType="{x:Type local:FirstControl}">
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:FirstControl}">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Height="490" DataContext="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Mode=OneTime}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="TEST TEXT" FontWeight="DemiBold"/>
<Button Command="{Binding Path=CloseCommand, Mode=OneTime}"
Width="48" Height="30"/>
</StackPanel>
</ScrollViewer>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Code Behind:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
namespace Presentation
{
/// <summary>
/// View-model
/// </summary>
public class FirstControl : Control
{
static FirstControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(FirstControl), new FrameworkPropertyMetadata(typeof(FirstControl)));
}
public FirstControl()
{
CloseCommand = new DelegateCommand(OnCloseCommand);
}
private void OnCloseCommand()
{
// Write code to close application.
}
public static readonly DependencyProperty CloseCommandProperty = DependencyProperty.Register("CloseCommand", typeof(ICommand), typeof(FirstControl));
public ICommand CloseCommand
{
get { return (ICommand)GetValue(CloseCommandProperty); }
set { SetValue(CloseCommandProperty, value); }
}
}
}
Hope this helps.
The DelegateCommand is a class to allow delegating command logic to methods passed as parameters.
The main point of MVVM is to allow each layer to be fully tested without the need of "higher" layers.
You should be able to test the Model, and in that test you should be able to successfully complete all the tasks required to send and retrieve data from your data store. Your model testing should not require any view or view-model to complete.
You should be able to test your View Model without the need for any UI code or other View level code. Your View Model should be able to logically do everything your application needs to do without any user interraction or UI code. Ideally, you should be able to test your ViewModel using mocked Model classes that provide predictable responses.

Caliburn.Micro wiring a View to a ViewModel for display by a ContentControl

I'm having some trouble with loading a view into a ContentControl. I'm trying to keep this as simple as possible so I used the Hello project that comes with CM. I made sure that the Hello project compiles correctly, and runs. It displays a window with a textbox, and a button. Both the textbox and button are wired at runtime to the sample ViewModel.
I modified the ShellView.xaml and replaced the StackPanel control with the Grid control, and setup the grid with 4 rows and a single column. I assigned the textbox to the first row, the button to the second row, and then two separate ContentControl to the final two rows.
<Grid Width="800" Height="600">
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBox Grid.Row="0" Grid.Column="0" x:Name="Name" />
<Button Grid.Row="1" Grid.Column="0" x:Name="SayHello" Content="Click Me" />
<ContentControl Grid.Row="2" Grid.Column="0" x:Name="TopMenu"
VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"></ContentControl>
<ContentControl Grid.Row="3" Grid.Column="0" x:Name="BottomMenu"
VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"></ContentControl>
</Grid>
I created two separate C# classes in the ViewModels folder which are the ViewModels and are respectively called TopMenuViewModel.cs, and BottomMenuViewModel.cs. Both classes extend the PropertyChangedBase class. This is simply mimicking the ShellViewModel.cs class that comes with the sample project.
using System;
using Caliburn.Micro;
namespace TestWithCaliburnMicro.ViewModels
{
/// <summary>
/// Description of BottomMenuViewModel.
/// </summary>
public class BottomMenuViewModel : PropertyChangedBase
{
public BottomMenuViewModel()
{
}
}
I created two separate WPF User Controls in the Views folder which are the corresponding View and are respectively called TopMenuView.xaml and BottomMenuView.xaml. I added a Label in each xaml with the Content of "Top Menu" or "Bottom Menu" to differentiate between the two.
<Grid>
<Label>Bottom Menu View</Label>
</Grid>
In the ShellViewModel.cs class I created two public properties with only the "get" accessor set to return an instance of the corresponding ViewModel.
private BottomMenuViewModel _bottomMenu;
public BottomMenuViewModel BottomMenu {
get { return _bottomMenu; }
}
private TopMenuViewModel _topMenu;
public TopMenuViewModel TopMenu {
get { return _topMenu;}
}
Adding a break to the get accessor of either property shows that the get accessor is called when debugging the project. I added a simple statement to the constructor of the BottomMenuViewModel.cs class, such as int x = 0 and added a break to that line, but the break is never hit which to me means that the constructor is not called, so really the class is not created?
I believe what I'm doing is exceptionally basic and have read the All About Conventions document on the CM Codeplex site, and confirmed the logic with this comment: Prior question on stackoverflow
Hopefully someone will have the time to read this and point me in the right direction. Thanks.
Solution on GitHub. Note: made with SharpDevelop 4.x
GitHub solution
Either instantiate your view models in the constructor of the ShellViewModel, or if you wish to instantiate them at a later point, then add setters to your view model properties, and call the NotifyOfPropertyChange method to notify your UI that those property references have changed.

How to build a generic/re-usable modal dialog for WPF following MVVM

I would like to build a generic/re-usable modal dialog that I can use in our WPF (MVVM) - WCF LOB application.
I have a Views and associated ViewModels that I would like to display using dialogs. Bindings between Views and ViewModels are done using Type-targeted DataTemplates.
Here are some requirements that I have been able to draft:
I prefer this to be based on a Window instead of using Adorners and controls that act like a modal dialog.
It should get its minimum size from the content.
It should center on the owner window.
The window must not show the Minimize and Maximize buttons.
It should get its title from the content.
What is the best way to do this?
I usually deal with this by injecting this interface into the appropriate ViewModels:
public interface IWindow
{
void Close();
IWindow CreateChild(object viewModel);
void Show();
bool? ShowDialog();
}
This allows the ViewModels to spaw child windows and show them modally on modeless.
A reusable implementation of IWindow is this:
public class WindowAdapter : IWindow
{
private readonly Window wpfWindow;
public WindowAdapter(Window wpfWindow)
{
if (wpfWindow == null)
{
throw new ArgumentNullException("window");
}
this.wpfWindow = wpfWindow;
}
#region IWindow Members
public virtual void Close()
{
this.wpfWindow.Close();
}
public virtual IWindow CreateChild(object viewModel)
{
var cw = new ContentWindow();
cw.Owner = this.wpfWindow;
cw.DataContext = viewModel;
WindowAdapter.ConfigureBehavior(cw);
return new WindowAdapter(cw);
}
public virtual void Show()
{
this.wpfWindow.Show();
}
public virtual bool? ShowDialog()
{
return this.wpfWindow.ShowDialog();
}
#endregion
protected Window WpfWindow
{
get { return this.wpfWindow; }
}
private static void ConfigureBehavior(ContentWindow cw)
{
cw.WindowStartupLocation = WindowStartupLocation.CenterOwner;
cw.CommandBindings.Add(new CommandBinding(PresentationCommands.Accept, (sender, e) => cw.DialogResult = true));
}
}
You can use this Window as a reusable host window. There's no code-behind:
<Window x:Class="Ploeh.Samples.ProductManagement.WpfClient.ContentWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:self="clr-namespace:Ploeh.Samples.ProductManagement.WpfClient"
xmlns:pm="clr-namespace:Ploeh.Samples.ProductManagement.PresentationLogic.Wpf;assembly=Ploeh.Samples.ProductManagement.PresentationLogic.Wpf"
Title="{Binding Path=Title}"
Height="300"
Width="300"
MinHeight="300"
MinWidth="300" >
<Window.Resources>
<DataTemplate DataType="{x:Type pm:ProductEditorViewModel}">
<self:ProductEditorControl />
</DataTemplate>
</Window.Resources>
<ContentControl Content="{Binding}" />
</Window>
You can read more about this (as well as download the full code sample) in my book.
I'm answering my own question to help others find all answers I struggled to find in one place. What above seems like a straight forward problem, actually presents multiple problems that I hope to answer sufficiently below.
Here goes.
Your WPF window that will serve as the generic dialog can look something like this:
<Window x:Class="Example.ModalDialogView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ex="clr-namespace:Example"
Title="{Binding Path=mDialogWindowTitle}"
ShowInTaskbar="False"
WindowStartupLocation="CenterOwner"
WindowStyle="SingleBorderWindow"
SizeToContent="WidthAndHeight"
ex:WindowCustomizer.CanMaximize="False"
ex:WindowCustomizer.CanMinimize="False"
>
<DockPanel Margin="3">
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" FlowDirection="RightToLeft">
<Button Content="Cancel" IsCancel="True" Margin="3"/>
<Button Content="OK" IsDefault="True" Margin="3" Click="Button_Click" />
</StackPanel>
<ContentPresenter Name="WindowContent" Content="{Binding}"/>
</DockPanel>
</Window>
Following MVVM, the right way to show a dialog is through a mediator. To use a mediator, you typically require some service locator as well. For mediator specific details, look here.
The solution I settled on involved implementing an IDialogService interface that is resolved through a simple static ServiceLocator. This excellent codeproject article has the details on that. Take note of this message in the article forum. This solution also solves the problem of discovering the owner window via the ViewModel instance.
Using this interface, you can call IDialogService.ShowDialog(ownerViewModel, dialogViewModel). For now, I'm calling this from the owner ViewModel, meaning I have hard references between my ViewModels. If you use aggregated events, you will probably call this from a conductor.
Setting the minimum size on the View that will eventually be displayed in the dialog doesn't automatically set the minimum size of the dialog. Also, since the logical tree in the dialog contains the ViewModel, you can't just bind to the WindowContent element's properties. This question has an answer with my solution.
The answer I mention above also includes code that centers the window on the owner.
Finally, disabling the minimize and maximize buttons is something WPF can't natively do. The most elegant solution IMHO is using this.

Resources