如何在WPF中编写一个通用的对话框窗口?

huangapple go评论49阅读模式
英文:

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.

我有一个基类,这个自定义控件没有默认模板 SlimWindow。它的作用很小 - 它移除了窗口标题栏和一些其他内容。

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

我有多个窗口从它继承,它们具有类似的行为 - 例如对话框窗口。它们有一个标题栏和确定/取消按钮。

Since I can't 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?

因为我不能继承一个正常的窗口(带有 xaml 和 cs),那么实现具有 XAML 代码后台的“子类”窗口具有相同功能的最佳方法是什么?

It should:

它应该:

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

能够为“基础”窗口提供TitleContent(位于标题和确定/取消按钮之间的部分)。

  • expose inner elements for styling (eg if I want to change OK button background color) 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.

在 XAML 中以尽可能自动化的方式集成到“基础”窗口中的几行。

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)

额外考虑:我使用DevExpress主题,所以我不确定Themes/generic.xaml中的自定义控件默认样式(按照教材)是否会起作用或者是否会被当前主题找到(它们在运行时是用户可选择的)

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>
英文:

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:

&lt;Window x:Class=&quot;DialogWindow.ADialogWindow&quot; ...&gt;
    &lt;Grid&gt;
        &lt;local:DialogWindowControl TitleContent=&quot;A Title&quot; OnOkClosed=&quot;onDialogOkClose&quot;&gt;
            &lt;Grid Background=&quot;Green&quot;&gt;
                &lt;TextBlock Text=&quot;Content&quot;/&gt;
            &lt;/Grid&gt;
        &lt;/local:DialogWindowControl&gt;
    &lt;/Grid&gt;
&lt;/Window&gt;

答案1

得分: 3

"Since I cant inherit a proper window (with xaml and cs)" - 由于这个错误的假设,看起来你的问题基于这一点。你可以继承自 Window,只是不要使用 XAML 定义基类。而是定义一个默认的 Style
Preferably you would create a custom control that extends Window and add the default Style to the Generic.xaml ResourceDictionary.
在这个 Style 中,你可以覆盖 ControlTemplate 来实现新的对话框外观(共享布局)。
继承新的基类的对话框将像往常一样将其内容添加到 Window.Content 属性中。

Dialog.cs
所有对话框的基类。它将在底部包含 "Ok" 和 "Cancel" 按钮。

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
添加默认的 Style,覆盖模板。
继承自 Dialog 的对话框将像往常一样将其内容添加到 Window.Content 属性中。
"Ok" 按钮将使用分配给 Button.Command 的路由 Dialog.DialogOkCommand。这个路由命令在 Dialog 基类中定义和处理。

<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

以下示例对话框将显示一个 DatePicker 和它从 Dialog 基类定义的 ControlTemplate 继承的外观。
在这种情况下,对话框将继承底部的 "Ok" 和 "Cancel" Button(在其自身的 DatePicker 内容之下),包括功能(因为它在基类 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
当使用基类 Dialog 作为对话框的根 XAML 元素时,
扩展的对话框 DatePickerDialog 将继承 Dialog 定义的模板。这意味着在这种情况下,它将包含一个 "Ok" 和 "Cancel" 按钮。

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

MainWindow.xaml.cs
示例显示如何处理对话框。

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
    }
  }
}
英文:

"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(&quot;Closes the dialog and sets the DialogResult to &#39;true&#39;&quot;, 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) 
    =&gt; e.CanExecute = CanCloseDialog(e.Parameter);

  private void ExecuteDialogOkCommand(object sender, ExecutedRoutedEventArgs e)
    =&gt; CloseDialog(e.Parameter);

  // Allow derived types to override/extend the behavior
  protected virtual bool CanCloseDialog(object commandParameter)
    =&gt; 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.

&lt;Style TargetType=&quot;local:Dialog&quot;&gt;
  &lt;Setter Property=&quot;Background&quot;
          Value=&quot;White&quot; /&gt;
  &lt;Setter Property=&quot;Template&quot;&gt;
    &lt;Setter.Value&gt;
      &lt;ControlTemplate TargetType=&quot;Window&quot;&gt;
        &lt;Border Background=&quot;{TemplateBinding Background}&quot;
                BorderBrush=&quot;{TemplateBinding BorderBrush}&quot;
                BorderThickness=&quot;{TemplateBinding BorderThickness}&quot;&gt;
          &lt;Grid&gt;
            &lt;Grid.RowDefinitions&gt;
              &lt;RowDefinition x:Name=&quot;DialogHeaderChromeRow&quot;
                             Height=&quot;Auto&quot; /&gt;
              &lt;RowDefinition x:Name=&quot;ContentRow&quot; /&gt;
              &lt;RowDefinition x:Name=&quot;DialogFooterChromeRow&quot;
                             Height=&quot;Auto&quot; /&gt;
            &lt;/Grid.RowDefinitions&gt;

            &lt;AdornerDecorator Grid.Row=&quot;1&quot;&gt;
              &lt;ContentPresenter /&gt;
            &lt;/AdornerDecorator&gt;

            &lt;StackPanel Grid.Row=&quot;2&quot;
                        Orientation=&quot;Horizontal&quot;&gt;
              &lt;Button Content=&quot;Ok&quot;
                      Command=&quot;{x:Static local:Dialog.DialogOkCommand}&quot; /&gt;

              &lt;!-- Because Button.IsCancel is &#39;true&#39;, clicking the Button 
                   will automatically close the dialog and set Window.DialogResult to &#39;false&#39; --&gt;
              &lt;Button Content=&quot;Cancel&quot;
                      IsCancel=&quot;True&quot;
                      IsDefault=&quot;True&quot; /&gt;
            &lt;/StackPanel&gt;
          &lt;/Grid&gt;
        &lt;/Border&gt;
      &lt;/ControlTemplate&gt;
    &lt;/Setter.Value&gt;
  &lt;/Setter&gt;
&lt;/Style&gt;

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)
    =&gt; 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.

&lt;local:Dialog x:Class=&quot;DatePickerDialog&quot;
              Title=&quot;DatePickerDialog&quot;
              Height=&quot;450&quot;
              Width=&quot;800&quot;&gt;
  &lt;DatePicker /&gt;
&lt;/local:Dialog&gt;

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 &quot;Ok&quot; button was clicked
      this.MainViewModel.CommitDatePickerViewModelChanges();
    }
    else
    {
      // Do something when the &quot;Cancel&quot; button was clicked
    }
  }
}

答案2

得分: 0

这是我迄今为止所做的工作(Devexpress主题正常工作):

自定义控件:

public class DialogCustomControl : ContentControl
{
    public DialogCustomControl()
    {
    }

    public event EventHandler<RoutedEventArgs> OkButtonClick;

    Button okButton;
    Window parentWindow;

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        okButton = GetTemplateChild("okButton") as Button;
        if (okButton != null)
        {
            okButton.Click += okButton_Click;
        }

        parentWindow = Window.GetWindow(this);
    }

    private void okButton_Click(object sender, RoutedEventArgs e)
    {
        if (OkButtonClick != null) OkButtonClick(this, e);
        parentWindow.Close();
    }

    public static object GetTitleContent(DependencyObject obj)
    {
        return (string)obj.GetValue(TitleContentProperty);
    }
    public static void SetTitleContent(DependencyObject obj, object value)
    {
        obj.SetValue(TitleContentProperty, value);
    }

    public static readonly DependencyProperty TitleContentProperty =
        DependencyProperty.RegisterAttached("TitleContent", typeof(object), typeof(DialogCustomControl), new PropertyMetadata(null));
}

app.xaml 中的模板:

<Style TargetType="{x:Type local:DialogCustomControl}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:DialogCustomControl}">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="*"/>
                        <RowDefinition Height="40"/>
                    </Grid.RowDefinitions>

                    <ContentPresenter Content="{TemplateBinding TitleContent}" Grid.Row="0" Margin="5"/>
                    <ContentPresenter Content="{TemplateBinding Content}" Grid.Row="1"/>
                    <StackPanel Orientation="Horizontal" Grid.Row="2" HorizontalAlignment="Right" Margin="5">
                        <Button Content="Cancel" Margin="0,0,5,0" Width="80"/>
                        <Button Content="OK" x:Name="okButton" Width="80"/>
                    </StackPanel>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

用法(可以将标题设置为字符串或XAML内容):

<Window x:Class="DialogWindow.ADialogWindow" ...>
    <Grid>
        <!-- TitleContent="A Title" -->
        <local:DialogCustomControl OkButtonClick="okButton_Click">
            <local:DialogCustomControl.TitleContent>
                <Border BorderBrush="Red" BorderThickness="2">
                    <TextBlock Text="A TITLE" HorizontalAlignment="Center" VerticalAlignment="Center"/>
                </Border>
            </local:DialogCustomControl.TitleContent>
            <Grid Background="Green">
                <TextBlock Text="Content"/>
            </Grid>
        </local:DialogCustomControl>
    </Grid>
</Window>
英文:

This is what I have done so far (Devexpress themes work just fine):

Custom control:

    public class DialogCustomControl : ContentControl
    {
        public DialogCustomControl()
        {
        }

        public event EventHandler&lt;RoutedEventArgs&gt; OkButtonClick;

        Button okButton;
        Window parentWindow;

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            okButton = GetTemplateChild(&quot;okButton&quot;) as Button;
            if (okButton != null)
            {
                okButton.Click += okButton_Click;
            }
        
            parentWindow = Window.GetWindow(this);
        }

        private void okButton_Click(object sender, RoutedEventArgs e)
        {
            if(OkButtonClick != null) OkButtonClick(this, e);
            parentWindow.Close();
        }

        public static object GetTitleContent(DependencyObject obj)
        {
            return (string)obj.GetValue(TitleContentProperty);
        }
        public static void SetTitleContent(DependencyObject obj, object value)
        {
            obj.SetValue(TitleContentProperty, value);
        }

        public static readonly DependencyProperty TitleContentProperty =
            DependencyProperty.RegisterAttached(&quot;TitleContent&quot;, typeof(object), typeof(DialogCustomControl), new PropertyMetadata(null));
    }

Template in app.xaml:

        &lt;Style TargetType=&quot;{x:Type local:DialogCustomControl}&quot;&gt;
            &lt;Setter Property=&quot;Template&quot;&gt;
                &lt;Setter.Value&gt;
                    &lt;ControlTemplate TargetType=&quot;{x:Type local:DialogCustomControl}&quot;&gt;
                        &lt;Grid&gt;
                            &lt;Grid.RowDefinitions&gt;
                                &lt;RowDefinition Height=&quot;40&quot;/&gt;
                                &lt;RowDefinition Height=&quot;*&quot;/&gt;
                                &lt;RowDefinition Height=&quot;40&quot;/&gt;
                            &lt;/Grid.RowDefinitions&gt;

                            &lt;ContentPresenter Content=&quot;{TemplateBinding TitleContent}&quot; Grid.Row=&quot;0&quot; Margin=&quot;5&quot;/&gt;
                            &lt;ContentPresenter Content=&quot;{TemplateBinding Content}&quot; Grid.Row=&quot;1&quot;/&gt;
                            &lt;StackPanel Orientation=&quot;Horizontal&quot; Grid.Row=&quot;2&quot; HorizontalAlignment=&quot;Right&quot; Margin=&quot;5&quot;&gt;
                                &lt;Button Content=&quot;Cancel&quot; Margin=&quot;0,0,5,0&quot; Width=&quot;80&quot;/&gt;
                                &lt;Button Content=&quot;OK&quot; x:Name=&quot;okButton&quot; Width=&quot;80&quot;/&gt;
                            &lt;/StackPanel&gt;

                        &lt;/Grid&gt;
                    &lt;/ControlTemplate&gt;
                &lt;/Setter.Value&gt;
            &lt;/Setter&gt;

        &lt;/Style&gt;

Usage (can set title as a string or as a XAML content):

&lt;Window x:Class=&quot;DialogWindow.ADialogWindow&quot; ... &gt;
    &lt;Grid&gt;
        &lt;!-- TitleContent=&quot;A Title&quot; --&gt;
        &lt;local:DialogCustomControl OkButtonClick=&quot;okButton_Click&quot;&gt;
            &lt;local:DialogCustomControl.TitleContent&gt;
                &lt;Border BorderBrush=&quot;Red&quot; BorderThickness=&quot;2&quot;&gt;
                    &lt;TextBlock Text=&quot;A TITLE&quot; HorizontalAlignment=&quot;Center&quot; VerticalAlignment=&quot;Center&quot;/&gt;
                &lt;/Border&gt;
            &lt;/local:DialogCustomControl.TitleContent&gt;
            &lt;Grid Background=&quot;Green&quot;&gt;
                &lt;TextBlock Text=&quot;Content&quot;/&gt;
            &lt;/Grid&gt;
        &lt;/local:DialogCustomControl&gt;
    &lt;/Grid&gt;
&lt;/Window&gt;

huangapple
  • 本文由 发表于 2023年5月21日 18:02:51
  • 转载请务必保留本文链接:https://go.coder-hub.com/76299316.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定