Search code examples
wpf.net-7.0

How to write a generic dialog window in WPF?


I have a base class which is custom control without default template SlimWindow. It has little function - it removes Window header and some other stuff.

I have multiple windows inheriting from it which has similar behaviour - eg dialog windows. they have a header and ok/cancel buttons.

Since I cant inherit a proper window (with xaml and cs) - whats the best way to achieve the same functionality where the "subclassed" window will have XAML and code-behind?

It should:

  • be able to provide Title and Content (part of the window between title and the Ok/Cancel button) for the "base" window.

  • expose inner elements for styling (eg if I want to change OK button background colour) without writing dep props for every single property that comes to mind

  • be able to run custom logic on Ok/Cancel/Close etc.

  • integrate into the "base" window in few lines of XAML as automatically as possible.

Additional consideration: I use devexpress themes, so I am not sure if a customcontrol default style in Themes/generic.xaml (as per the textbook) will work and/or be found by the current theme (they are user-selectable in run-time)

Example of what I think the usage should look like:

<Window x:Class="DialogWindow.ADialogWindow" ...>
    <Grid>
        <local:DialogWindowControl TitleContent="A Title" OnOkClosed="onDialogOkClose">
            <Grid Background="Green">
                <TextBlock Text="Content"/>
            </Grid>
        </local:DialogWindowControl>
    </Grid>
</Window>

Solution

  • "Since I cant inherit a proper window (with xaml and cs)" - It looks like your problem is based on this wrong assumption. You can inherit from a Window, just don't define the base class using XAML. Instead define a default Style.
    Preferably you would create a custom control that extends Window and add the default Style to the Generic.xaml ResourceDictionary.
    In this Style you would override the ControlTemplate to implement the new dialog chrome (shared layout).
    Dialogs that extend the new base class will add their content to the Window.Content property as usual.

    Dialog.cs
    The base class for all dialogs. It will contain a "Ok" and "Cancel" button at the bottom.

    public class Dialog : Window
    {
      public static RoutedCommand DialogOkCommand { get; }
    
      static Dialog()
      {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(Dialog), new FrameworkPropertyMetadata(typeof(Dialog)));
        Dialog.DialogOkCommand = new RoutedUICommand("Closes the dialog and sets the DialogResult to 'true'", nameof(DialogOkCommand), typeof(Dialog));
      }
    
      public Dialog()
      {
        var dialogOkCommandBinding = new CommandBinding(
          Dialog.DialogOkCommand,
          ExecuteDialogOkCommand,
          CanExecuteDialogOkCommand);
        this.CommandBindings.Add(dialogOkCommandBinding);
      }
    
      private void CanExecuteDialogOkCommand(object sender, CanExecuteRoutedEventArgs e) 
        => e.CanExecute = CanCloseDialog(e.Parameter);
    
      private void ExecuteDialogOkCommand(object sender, ExecutedRoutedEventArgs e)
        => CloseDialog(e.Parameter);
    
      // Allow derived types to override/extend the behavior
      protected virtual bool CanCloseDialog(object commandParameter)
        => true;
    
      // Allow derived types to override/extend the behavior
      protected virtual void CloseDialog(object commandParameter)
      {
        this.DialogResult = true;  
        Close();
      }
    }
    

    Generic.xaml
    Add the default Style that overrides the template.
    Dialogs that extend Dialog will add their content to the Window.Content property as usual.
    The "Ok" button will use the routed Dialog.DialogOkCommand assigned to Button.Command. The routed command is defined and handled in the Dialog base class.

    <Style TargetType="local:Dialog">
      <Setter Property="Background"
              Value="White" />
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="Window">
            <Border Background="{TemplateBinding Background}"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}">
              <Grid>
                <Grid.RowDefinitions>
                  <RowDefinition x:Name="DialogHeaderChromeRow"
                                 Height="Auto" />
                  <RowDefinition x:Name="ContentRow" />
                  <RowDefinition x:Name="DialogFooterChromeRow"
                                 Height="Auto" />
                </Grid.RowDefinitions>
    
                <AdornerDecorator Grid.Row="1">
                  <ContentPresenter />
                </AdornerDecorator>
    
                <StackPanel Grid.Row="2"
                            Orientation="Horizontal">
                  <Button Content="Ok"
                          Command="{x:Static local:Dialog.DialogOkCommand}" />
    
                  <!-- Because Button.IsCancel is 'true', clicking the Button 
                       will automatically close the dialog and set Window.DialogResult to 'false' -->
                  <Button Content="Cancel"
                          IsCancel="True"
                          IsDefault="True" />
                </StackPanel>
              </Grid>
            </Border>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>
    

    Usage example

    The following example dialog will display a DatePicker and the chrome it inherits from the ControlTemplate defined by the Dialog base class.
    In this case, the dialog will inherit the "Ok" and "Cancel" Button located at the bottom (below its own DatePicker content) including the functionality (because it is implemented in the base class Dialog).

    DatePickerDialog.xaml.cs

    public partial class DatePickerDialog : Dialog
    {
      public DatePickerDialog()
      {
        InitializeComponent();
      }
    
      // TODO::Optionally override CanCloseDialog and CloseDialog
      // in case we want to extend the behavior of the Dialog base class.
    
      protected override bool CanCloseDialog(object commandParameter)
        => this.IsSelectedDateValid;
    }
    

    DatePickerDialog.xaml
    When using the base class Dialog as root XAML element of the dialog, the extended dialog DatePickerDialog will inherit the template of the Dialog. This means in this case it will contain a "Ok" and "Cancel" button.

    <local:Dialog x:Class="DatePickerDialog"
                  Title="DatePickerDialog"
                  Height="450"
                  Width="800">
      <DatePicker />
    </local:Dialog>
    

    MainWindow.xaml.cs
    Example that shows how to handle the dialog.

    partial class MainWindow : Window
    {
      private MainViewModel MainViewModel { get; }
    
      public MainWindow()
      {
        InitializeComponent();
        this.MainViewModel = new MainViewModel();
        this.DataContext = this.MainViewModel;
      }
    
      private void OnShowDialogClicked(object sender, RoutedEventArgs e)
      {
        var datePickerDialog = new DatePickerDialog() 
        { 
          DataContext = this.MainViewModel.DatePickerDialogViewModel 
        };
    
        bool dialogResult = datePickerDialog.ShowDialog();
        if (dialogResult)
        {
          // Do something when the "Ok" button was clicked
          this.MainViewModel.CommitDatePickerViewModelChanges();
        }
        else
        {
          // Do something when the "Cancel" button was clicked
        }
      }
    }