0

I am an experienced WinForms developer, relatively new to WPF. I have a large WinForms application that uses a couple different base classes to represent dialog boxes. One such example is AbstractOkCancelDialog. That class contains a panel at the bottom of a dialog, with an Ok and Cancel button on the right side of the panel. I'm trying to determine the best way to handle this, as I realize that WPF doesn't provide visual inheritance.

I don't want to have to create OK and Cancel buttons, and place them, for every dialog in the application.

I have read that the way to do this in WPF is with user controls. I can envision creating a user control with OK and Cancel buttons on it. But I don't want to have to manually place that user control on hundreds of dialogs in my application. I'd really like to have something like this:

public AbstractOkCancelDialog = class(Window)
{
  protected AbstractOkCancelDialogViewModel _ViewModel;

  // AbstractOkCancelDialogViewModel would have commands for OK and Cancel.
  // Every dialog would inherit from AbstractOkCancelDialog, and would use
  // a viewmodel that inherits from AbstractOkCancelDialogViewModel. In 
  // this way, all view models would automatically be connected to the OK
  // and Cancel commands.
}

I've seen some discussion online about how to create the base class. Those discussions explain how there can't be a xaml file associated with the dialog base class, and I understand that restriction. I just can't figure out how to automatically place the user control with the OK and Cancel buttons.

I'm hoping that someone can point me to a sample solution that shows this kind of structure. Thank you in advance!

Eric Harmon
  • 175
  • 1
  • 10

3 Answers3

0

Write one dialog class. It's a subclass of Window. It has XAML:

<Window
    ...blah blah blah...
    Title="{Binding Title}"
    >
    <StackPanel MinWidth="300">
        <!--  This is how you place content within content in WPF -->
        <ContentControl
            Content="{Binding}"
            Margin="2"
            />
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="2,20,2,2">
            <Button 
                Margin="2" 
                MinWidth="60" 
                DockPanel.Dock="Right" 
                Content="OK" 
                Click="OK_Click" 
                IsDefault="True" 
                />
            <Button 
                Margin="2" 
                MinWidth="60" 
                DockPanel.Dock="Right" 
                Content="Cancel" 
                IsCancel="True" 
                Click="Cancel_Click" 
                />
        </StackPanel>
    </StackPanel>
</Window>

You can fancy that up endlessly, but this is a decent minimum to give you arbitrary content above a row of right-aligned buttons. Adding more buttons as needed could involve either templating that portion of the window as well, or creating them with an ItemsControl (I've done that in our production code), or a few other options.

Usage:

var vm = new SomeDialogViewModel();
var dlg = new MyDialog { DataContext = vm };

For each dialog viewmodel, consumers must define an implicit datatemplate which provides UI for that viewmodel.

I would suggest writing a dialog viewmodel interface which the consumer is expected to implement.

public interface IDialogViewModel
{
    String Title { get; set; }
    void OnOK();

    //  Let them "cancel the cancel" if they like. 
    bool OnCancel();
}

The window can check if its DataContext implements that interface, and act accordingly. If you like, it could require that interface and throw an exception of it isn't implemented, or it could just talk to it only if it's there. If they don't implement it but they still have a Title property, the binding to Title will still work. Bindings are "duck typed".

Naturally, you can write an OKCancelDialogViewModel or a SelectStringFromListViewModel, write corresponding DataTemplates that implement their UIs, and write nice clean static methods which show them:

public static class Dialogs
{
    public static TOption Select<TOption>(IEnumerable<TOption> options, string prompt,
        string title = "Select Option") where TOption : class
    {
        //  Viewmodel isn't generic because that breaks implicit datatemplating.
        //  That's OK because XAML uses duck typing anyhow. 
        var vm = new SelectOptionDialogViewModel
        {
            Title = title,
            Prompt = prompt,
            Options = options
        };

        if ((bool)new Dialog { DataContext = vm }.ShowDialog())
        {
            return vm.SelectedOption as TOption;
        }

        return null;
    }

    // We have to call the value-type overload by a different name because overloads can't be 
    // distinguished when the only distinction is a type constraint. 
    public static TOption? SelectValue<TOption>(IEnumerable<TOption> options, string prompt,
        string title = "Select Option") where TOption : struct
    {
        var vm = new SelectOptionDialogViewModel
        {
            Title = title,
            Prompt = prompt,
            //  Need to box these explicitly
            Options = options.Select(opt => (object)opt)
        };

        if ((bool)new Dialog { DataContext = vm }.ShowDialog())
        {
            return (TOption)vm.SelectedOption;
        }

        return null;
    }
}

Here's a viewmodel datatemplate for the above selection dialog:

<Application.Resources>
    <DataTemplate DataType="{x:Type local:SelectOptionDialogViewModel}">
        <StackPanel>
            <TextBlock
                TextWrapping="WrapWithOverflow"
                Text="{Binding Prompt}"
                />
            <ListBox
                ItemsSource="{Binding Options}"
                SelectedItem="{Binding SelectedOption}"
                MouseDoubleClick="ListBox_MouseDoubleClick"
                />
        </StackPanel>
    </DataTemplate>
</Application.Resources>

App.xaml.cs

private void ListBox_MouseDoubleClick(object sender, 
    System.Windows.Input.MouseButtonEventArgs e)
{
    ((sender as FrameworkElement).DataContext as IDialogViewModel).DialogResult = true;
}
var a = Dialogs.Select(new String[] { "Bob", "Fred", "Ginger", "Mary Anne" }, 
            "Select a dance partner:");
var b = Dialogs.SelectValue(Enum.GetValues(typeof(Options)).Cast<Options>(), 
            "Select an enum value:");
  • Thank you, everyone, for the detailed code. I'm going to play with these one by one, and see which one seems to match my coding style the best. – Eric Harmon Oct 10 '19 at 20:04
0

Here an example of how to use a custom AlertDialog
UserControl

<UserControl x:Class="Library.Views.AlertMessageDialogView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:p="clr-namespace:Library.Properties" 
             DataContext="{Binding RelativeSource={RelativeSource Self}}"       
             FlowDirection = "{Binding WindowFlowDirection, Mode=TwoWay}">    

    <Grid Background="{DynamicResource WindowBackgroundBrush}">
        <Canvas HorizontalAlignment="Left" Height="145" VerticalAlignment="Top" Width="385">
            <Label HorizontalAlignment="Left" Height="57" VerticalAlignment="Top" Width="365" Canvas.Left="10" Canvas.Top="10" FontSize="14" >
                <TextBlock x:Name="txtVocabAnglais" TextWrapping="Wrap" Text="{Binding Message, Mode=TwoWay}" Width="365" Height="57"  />
            </Label>
            <Button x:Name="cmdCancel" Content="{x:Static p:Resources.AlertMessageDialogViewcmdCancel}" Height="30" Canvas.Left="163" Canvas.Top="72" Width="71" Command = "{Binding CancelCommand}" CommandParameter = "null" IsDefault="True"/>
        </Canvas>
    </Grid>

</UserControl>

ViewModel Class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Windows;
using System.ComponentModel;
using System.Windows.Controls;

namespace Library.ViewModel
{
    public class AlertMessageDialogViewModel : BindableBaseViewModel
    {
        public event EventHandler CloseWindowEvent;

        private string _title;
        private string _message;

        public BaseCommand<string> YesCommand { get; private set; }
        public BaseCommand<string> CancelCommand { get; private set; }
        private WinformsNameSpace.FlowDirection _windowFlowDirection;

        public AlertMessageDialogViewModel()
        {
            CancelCommand = new BaseCommand<string>(cmdCancelBtnClick);
            WindowFlowDirection = CustomFuncVar.WindowFlowDirection;
        }

        public WinformsNameSpace.FlowDirection WindowFlowDirection
        {
            get
            {
                return _windowFlowDirection;
            }
            set
            {
                _windowFlowDirection = value;
                OnPropertyChanged("WindowFlowDirection");
            }
        }

        public string Message
        {
            get
            {
                return _message;
            }
            set
            {
                _message = value;
                OnPropertyChanged("Message");
            }
        }

        public string Title
        {
            get
            {
                return _title;
            }

            set
            {
                _title = value;
            }
        }

        private void cmdCancelBtnClick(string paramerter)
        {
            if (CloseWindowEvent != null)
                CloseWindowEvent(this, null);
        }

    }
}

DialogMessage Class

using System;
using System.Windows;
using System.Collections.Generic;

namespace Library.Helpers
{
    public static class DialogMessage 
    {
        public static void AlertMessage(string message, string title, Window OwnerWindowView)
        {
            try
            {
                //Affichage de méssage de succès enregistrement
                AlertMessageDialogViewModel alertDialogVM = new AlertMessageDialogViewModel();
                alertDialogVM.Message = message;
                alertDialogVM.Title = title;

                // Auto Generation Window
                FrameworkElement view = LpgetCustomUI.AutoGetViewFromName("AlertMessageDialogView");
                view.DataContext = alertDialogVM;
                Dictionary<string, object> localVarWindowProperty = new Dictionary<string, object>();
                localVarWindowProperty = LpgetCustomUI.GetWindowPropretyType_400x145(Properties.Resources.ApplicationTitle);
                CummonUIWindowContainer alertDialogView = new CummonUIWindowContainer(view, null, false, localVarWindowProperty);
                //End Auto Generation Window

                // Attachement de l'évènement de fermture de View au modèle
                alertDialogVM.CloseWindowEvent += new EventHandler(alertDialogView.fnCloseWindowEvent);

                if (OwnerWindowView!=null)
                {
                    alertDialogView.Owner = OwnerWindowView;
                }
                else
                {
                    alertDialogView.WindowStartupLocation = WindowStartupLocation.CenterScreen;
                }

                alertDialogView.ShowDialog();
            }
            catch (Exception ex)
            {

            }
        }
    } 
}


CummonUIWindowContainer Class

namespace CummonUILibrary.CummonUIHelpers
{
    public class CummonUIWindowContainer : Window
    {
        public event RoutedEventHandler CmbRootEvtLanguageChange;
        private FrameworkElement currentView;
        private ContentControl _contentcontainer;

        public CummonUIWindowContainer(string usercontrolName)
        {
            Contentcontainer = new ContentControl();
            currentView = new FrameworkElement();
        }

        public CummonUIWindowContainer()
        {
            Contentcontainer = new ContentControl();
            currentView = new FrameworkElement();
        }

        public CummonUIWindowContainer(FrameworkElement view, object model, bool setDataContextToView, Dictionary<string, object> WindowPropertyList)
        {
            Contentcontainer = new ContentControl();
            Contentcontainer.Name = "ContentControl";

            SetWindowProperty(view, model, setDataContextToView, WindowPropertyList);
        }

        public void SetWindowProperty(FrameworkElement view, object model, bool setDataContextToView, Dictionary<string, object> WindowPropertyList)
        {
            try
            {

                LinearGradientBrush brush = new LinearGradientBrush();
                GradientStop gradientStop1 = new GradientStop();
                gradientStop1.Offset = 0;
                gradientStop1.Color = Colors.Yellow;
                brush.GradientStops.Add(gradientStop1);

                GradientStop gradientStop2 = new GradientStop();
                gradientStop2.Offset = 0.5;
                gradientStop2.Color = Colors.Indigo;
                brush.GradientStops.Add(gradientStop2);
                GradientStop gradientStop3 = new GradientStop();
                gradientStop3.Offset = 1;
                gradientStop3.Color = Colors.Yellow;
                brush.GradientStops.Add(gradientStop3);
                this.Background = brush;

                CurrentView = view;
                Type elementType = this.GetType();
                ICollection<string> WindowPropertyListNames = WindowPropertyList.Keys;

                foreach (string propertyName in WindowPropertyListNames)
                {
                    PropertyInfo property = elementType.GetProperty(propertyName);
                    property.SetValue(this, WindowPropertyList[propertyName]);                    
                }

                if (setDataContextToView == true & model != null)
                {
                    CurrentView.DataContext = model;
                }
                if (CurrentView != null)
                {
                    Contentcontainer.Content = CurrentView;
                }

                //Contentcontainer.Margin = new Thickness(0,0, 0, 0);
                IAddChild container=this;
                container.AddChild(Contentcontainer);                

            }
            catch (Exception ex)
            {
            }
        }

        public void fnCloseWindowEvent(object sender, EventArgs e)
        {
            this.Close();
        }

        public ContentControl Contentcontainer
        {
            get
            {
                return _contentcontainer;
            }
            set
            {
                _contentcontainer = value;
            }
        }

        public FrameworkElement CurrentView
        {
            get
            {
                return currentView;
            }
            set
            {
                if (this.currentView != value)
                {
                    currentView = value;
                    //RaisePropertyChanged("CurrentView");
                }
            }
        }   

        private void cmbLanguage_SelectionChanged(object sender, RoutedEventArgs e)
        {
            //CmbRootEvtLanguageChange(sender, e);
        }

    }
}

How to use the Class

DialogMessage.AlertMessage("My Custom Message", "My Custom Title Message");
LPGTE SOFTS
  • 120
  • 8
  • Thank you, everyone, for the detailed code. I'm going to play with these one by one, and see which one seems to match my coding style the best. – Eric Harmon Oct 10 '19 at 20:04
0

Thats how i would do it

Create an abstract base class for your dialog and changing the corresponding ControlTemplate

AbstractOkCancelDialog

public abstract class AbstractOkCancelDialog : Window
{
    public static readonly DependencyProperty CancelCommandParameterProperty =
        DependencyProperty.Register(
            "CancelCommandParameter",
            typeof(object),
            typeof(AbstractOkCancelDialog),
            new FrameworkPropertyMetadata((object) null));

    public static readonly DependencyProperty CancelCommandProperty =
        DependencyProperty.Register(
            "CancelCommand",
            typeof(ICommand),
            typeof(AbstractOkCancelDialog),
            new FrameworkPropertyMetadata((ICommand) null));

    public static readonly DependencyProperty OkCommandParameterProperty =
        DependencyProperty.Register(
            "OkCommandParameter",
            typeof(object),
            typeof(AbstractOkCancelDialog),
            new FrameworkPropertyMetadata((object) null));

    public static readonly DependencyProperty OkCommandProperty =
        DependencyProperty.Register(
            "OkCommand",
            typeof(ICommand),
            typeof(AbstractOkCancelDialog),
            new FrameworkPropertyMetadata((ICommand) null));


    static AbstractOkCancelDialog()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(AbstractOkCancelDialog), new
            FrameworkPropertyMetadata(typeof(AbstractOkCancelDialog)));
    }

    public ICommand CancelCommand
    {
        get => (ICommand) GetValue(CancelCommandProperty);
        set => SetValue(CancelCommandProperty, value);
    }

    public object CancelCommandParameter
    {
        get => GetValue(CancelCommandParameterProperty);
        set => SetValue(CancelCommandParameterProperty, value);
    }

    public ICommand OkCommand
    {
        get => (ICommand) GetValue(OkCommandProperty);
        set => SetValue(OkCommandProperty, value);
    }

    public object OkCommandParameter
    {
        get => GetValue(OkCommandParameterProperty);
        set => SetValue(OkCommandParameterProperty, value);
    }
}

Style

Put in Generic.xaml[?]

<Style
    BasedOn="{StaticResource {x:Type Window}}"
    TargetType="{x:Type local:AbstractOkCancelDialog}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:AbstractOkCancelDialog}">
                <Border
                    Background="{TemplateBinding Background}"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}">
                    <AdornerDecorator>
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="*" />
                                <RowDefinition Height="Auto" />
                            </Grid.RowDefinitions>
                            <ContentPresenter />
                            <Grid Grid.Row="1">
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="*" />
                                    <ColumnDefinition Width="Auto" />
                                    <ColumnDefinition Width="Auto" />
                                </Grid.ColumnDefinitions>
                                <Button
                                    Grid.Column="1"
                                    Margin="5"
                                    Command="{TemplateBinding OkCommand}"
                                    CommandParameter="{TemplateBinding OkCommandParameter}"
                                    Content="Ok"
                                    DockPanel.Dock="Right" />
                                <Button
                                    Grid.Column="2"
                                    Margin="5"
                                    Command="{TemplateBinding CancelCommand}"
                                    CommandParameter="{TemplateBinding CancelCommandParameter}"
                                    Content="Cancel"
                                    DockPanel.Dock="Right" />
                            </Grid>
                        </Grid>
                    </AdornerDecorator>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Now you can create your individual dialogs like you would create any other window

Brief example:

TestDialog.xaml

<local:AbstractOkCancelDialog
    x:Class="WpfApp.TestDialog"
    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:local="clr-namespace:WpfApp"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="TestDialog"
    Width="800"
    Height="450"
    OkCommand="{x:Static local:Commands.OkWindowCommand}"
    OkCommandParameter="{Binding RelativeSource={RelativeSource Self}}"
    CancelCommand="{x:Static local:Commands.CancelWindowCommand}"
    CancelCommandParameter="{Binding RelativeSource={RelativeSource Self}}"
    mc:Ignorable="d">
    <Grid>
        <!-- Content -->
    </Grid>
</local:AbstractOkCancelDialog>

TestDialog.xaml.cs

public partial class TestDialog : AbstractOkCancelDialog
{
    ...
}
nosale
  • 808
  • 6
  • 14
  • Thank you, everyone, for the detailed code. I'm going to play with these one by one, and see which one seems to match my coding style the best. – Eric Harmon Oct 10 '19 at 20:04