WPF Dialogs

by MikeHogg 15. July 2010 21:13

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.

Tags:

WPF

About Mike Hogg

Mike Hogg is a c# developer in Brooklyn.

More Here

Favorite Books

This book had the most influence on my coding style. It drastically changed the way I write code and turned me on to test driven development even if I don't always use it. It made me write clearer, functional-style code using more principles such as DRY, encapsulation, single responsibility, and more. amazon.com

This book opened my eyes to a methodical and systematic approach to upgrading legacy codebases step by step. Incrementally transforming code blocks into testable code before making improvements. amazon.com

More Here