Here’s the modal dialog I used in my WPF projects, where a windowless popup obstructs the application and the app is dimmed and disabled in the background until the user responds...
The view root element uses a grid sized to the entire parent window and only slightly opaque (much like I’ve seen this done in web pages) and the actual dialog is a smaller grid inside that.
<DataTemplate x:Key="Dialog" DataType="DialogVM">
<Grid Height="{Binding Height, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}"
Width="{Binding Width, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}">
<Grid.Background>
<SolidColorBrush Color="LightGray" Opacity=".6"></SolidColorBrush>
</Grid.Background>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="50"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="100"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Border HorizontalAlignment="Center" BorderBrush="Black" BorderThickness="1"
Background="LightSkyBlue" Height="120" Width="320"
Grid.Column="1" Grid.Row="1" CornerRadius="10" >
<Border.Effect>
<DropShadowEffect ShadowDepth="10"/>
</Border.Effect>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition Height="100"></RowDefinition>
</Grid.RowDefinitions>
<Border BorderThickness="0,0,0,1" Grid.Row="0"><!-- BorderBrush="Black"--><!-- not using border looks too much like a window the user wants to drag it -->
<TextBlock Text="{Binding Title}" Margin="10,3,0,0"></TextBlock>
</Border>
<Grid Grid.Row="1" >
<Grid.RowDefinitions>
<RowDefinition Height="60"></RowDefinition>
<RowDefinition Height="40"></RowDefinition>
</Grid.RowDefinitions>
<Grid Background="AliceBlue"Grid.Row="0" >
<TextBlock Text="{Binding Message}" FontSize="14" TextWrapping="Wrap"
Margin="10" TextAlignment="Center" ></TextBlock>
</Grid>
<Border Background="AliceBlue" Grid.Row="1">
<Border.CornerRadius>
<CornerRadius BottomLeft="10" BottomRight="10"></CornerRadius>
</Border.CornerRadius>
<Button Content="OK" Command="{Binding CmdOK}" Width="50" Height="20" ></Button>
</Border>
</Grid>
</Grid>
</Border>
</Grid>
</DataTemplate>
I use one for messages, and an extended one for Error Messages. I put these anywhere at the bottom of one of my WPF windows, like this
<ContentControl Content="{Binding Dialog}" ContentTemplate="{StaticResource Dialog}"
Grid.RowSpan="2" Visibility="{Binding Dialog.Visibility}">
</ContentControl>
<ContentControl Content="{Binding Error}" ContentTemplate="{StaticResource Error}"
Grid.RowSpan="2" Visibility="{Binding Error.Visibility}">
</ContentControl>
</Grid>
</DataTemplate>
</ResourceDictionary>
And add the class as a Property to the window’s ViewModel:
public class SomeViewModel
{
#region Fields
...
DialogVMBase _dialog;
ErrorVM _error;
#endregion
#region Properties
...
public DialogVMBase Dialog
{
get
{
return _dialog;
}
set
{
this._dialog = value;
this._dialog.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(_dialog_PropertyChanged);
this.OnPropertyChanged("Dialog"); // this one sends notice to Binding to open the dialog
}
}
public ErrorVM Error
{
get
{
return _error;
}
set
{
this._error = value;
this._error.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(_error_PropertyChanged);
this.OnPropertyChanged("Error");
}
}
// simple button OK clicks change Visibility property and we catch dialogVM.PropertyChanged here
void _dialog_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
this.OnPropertyChanged("Dialog");// this one sends notice to ListVM Binding (to close dialog)
}
void _error_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
this.OnPropertyChanged("Error");
}
#endregion
You see there we attach a delegate in the parent window to PropertyChanged on the Dialog, so when a property changes in the Dialog, the parent is notified. In the Dialog next you will see clicking OK just sets the Visibility to Hidden and calls PropertyChanged, which will then call the parent window’s event handler, whose Model Binder notices the Visibility property is changed, and then registers the Hide in the UI.
public class DialogVMBase : ViewModelBase
{
#region fields
string _title;
string _message;
System.Windows.Visibility _visibility;
RelayCommand _cmdok;
#endregion
// derived classes must set Visible explicitly in their own constructor
public DialogVMBase() {
this._visibility = System.Windows.Visibility.Hidden;
}
// this contructor gets Visible by default
public DialogVMBase(string title, string message)
{
this.Message = message;
this.Title = title;
}
#region Properties
public virtual string Title
{
get { return _title; }
set
{
SetProperty(ref _title, value, "Title");
}
}
public virtual string Message
{
get { return _message; }
set
{
SetProperty(ref _message, value, "Message");
}
}
public virtual System.Windows.Visibility Visibility
{
get { return _visibility; }
set
{
if (_visibility == value) return;
_visibility = value;
this.OnPropertyChanged("Visibility");
}
}
#endregion
public event EventHandler ButtonOkClicked;
#region Commands
public virtual RelayCommand CmdOK
{
get
{
if (_cmdok == null)
{
_cmdok = new RelayCommand(
param => this.OK(),
param => this.OKEnabled()
);
}
return _cmdok;
}
}
public virtual bool OKEnabled()
{
return true;
}
public virtual void OK()
{
EventHandler handler = ButtonOkClicked;
if (handler != null) handler(this, null);
this.Visibility = System.Windows.Visibility.Hidden;
}
#endregion
}
In my ErrorVM I add some more exception properties
public class ErrorVM : DialogVMBase
{
#region fields
Exception _exception;
string _stack;
Exception _inner;
Dictionary<object,object> _data;
#endregion
public ErrorVM() { }
public ErrorVM(Exception e)
{
if (e != null)
{
_exception = e;
this.Title = "Error";
this.Message = e.Message;
_stack = e.StackTrace;
_inner = e.InnerException;
_data = new Dictionary<object, object>();
// on rethrow the first exception sometimes becomes an inner
while (e.Data.Count == 0 && e.InnerException != null)
{
e = e.InnerException;
}
foreach (System.Collections.DictionaryEntry d in e.Data)
_data.Add(d.Key, d.Value);
this.Visibility = System.Windows.Visibility.Visible;
}
}
#region Properties
public string StackTrace
{
get { return _stack; }
set
{
if (_stack == value) return;
_stack = value;
this.OnPropertyChanged("StackTrace");
}
}
public Exception InnerException
{
get { return _inner; }
set
{
if (_inner == value) return;
_inner = value;
this.OnPropertyChanged("InnerException");
}
}
public Dictionary<object,object> Data
{
get { return _data; }
set
{
if (_data == value) return;
_data = value;
this.OnPropertyChanged("Data");
}
}
#endregion
}
and then the parent can call it at any time, like this:
if (MatchGroups.Count == 0) Dialog = new DialogVMBase("No Results",
String.Format("No Results found for your Product: {0}", SelectedProductName));
and that’s it.