Laden...

[Snippet] Nicht-modale Abfrage als Alternative für MessageBoxen

Erstellt von herbivore vor 11 Jahren Letzter Beitrag vor 10 Jahren 12.517 Views
herbivore Themenstarter:in
49.485 Beiträge seit 2005
vor 11 Jahren
[Snippet] Nicht-modale Abfrage als Alternative für MessageBoxen

Beschreibung:

Wie in Warten auf Schließen einer anderen Form [und warum man Dialoge nicht modal machen sollte] beschrieben und begründet, sind modale Dialoge und MessageBoxen out und es gibt mittlerweile bessere Alternativen. Hier zeige ich ein kleines Snippet für die übliche Abfrage beim Schließen eines Fenstern mit ungespeicherten Änderungen (siehe auch den Screenshot weiter unten). Statt in einer MessageBox werden die Buttons, mit denen der Benutzer die Abfrage beantworten kann, direkt in das Form eingeblendet. Der Vorteil liegt darin, dass das Form während der Abfrage voll bedienbar bleibt. Es können also z.B. noch letzte Änderungen vorgenommen werden, bevor der Benutzer diese endgültig speichert. Außerdem bleibt das Form die ganze Zeit auf dem Bildschirm frei verschiebbar.

[EDIT]Das grundlegende Prinzip lässt sich für jede Art von (Sicherheits-)Abfragen und außerdem auch für alle Hinweis-, Warnungs- und Fehlermeldungen nutzen. Eben überall dort, wo man bisher MessageBoxen (oder InputBoxen) verwendet hat.[/EDIT]


using System;
using System.Windows.Forms;
using System.Drawing;
//using System.Runtime.InteropServices;

//*****************************************************************************
public class EditorForm : Form
{
   //--------------------------------------------------------------------------
   private const    int    Spacing = 10;
   private readonly Panel  closeActions;
   private readonly int    closeActionsWidth;
   private readonly Button saveAndCloseButton;
   private          bool   delayClose = true;

   //==========================================================================
   public EditorForm ()
   {
      Control container;
      Control previousControl;

      Text = "Editor - Unbekanntes Dokument";
      ClientSize = new Size (640, 480);

      {
         var currentControl = new RichTextBox ();
         currentControl.Dock = DockStyle.Fill;
         Controls.Add (currentControl);
         previousControl = currentControl;
      }

      {
         var currentControl = closeActions = new Panel ();
         currentControl.Dock = DockStyle.Top;
         currentControl.Visible = false;
         currentControl.BackColor = Color.FromArgb (165, 201, 239);
         Controls.Add (currentControl);
         container = currentControl;
         previousControl = currentControl;
      }
      {
         var currentControl = new Label ();
         currentControl.Text = "Welche Aktion möchten Sie durchführen?";
         currentControl.Top = Spacing;
         currentControl.Left = Spacing;
         currentControl.AutoSize = true;
         container.Controls.Add (currentControl);
         previousControl = currentControl;
      }
      {
         var currentControl = saveAndCloseButton = new Button ();
         currentControl.Text = "Speichern und schließen";
         currentControl.Top = previousControl.Bottom + Spacing;
         currentControl.Left = previousControl.Left;
         currentControl.AutoSize = true;
         currentControl.FlatStyle = FlatStyle.Flat;
         currentControl.Click += SaveAndCloseClick;
         container.Controls.Add (currentControl);
         previousControl = currentControl;
         AcceptButton = currentControl;
      }
      {
         var currentControl = new Button ();
         currentControl.Text = "Schließen ohne zu speichern";
         currentControl.Top = previousControl.Top;
         currentControl.Left = previousControl.Right + Spacing;
         currentControl.AutoSize = true;
         currentControl.FlatStyle = FlatStyle.Flat;
         currentControl.Click += CloseOnlyClick;
         container.Controls.Add (currentControl);
         previousControl = currentControl;
      }
      {
         var currentControl = new Button ();
         currentControl.Text = "Keine";
         currentControl.Top = previousControl.Top;
         currentControl.Left = previousControl.Right + Spacing;
         currentControl.AutoSize = true;
         currentControl.FlatStyle = FlatStyle.Flat;
         currentControl.Click += DontCloseClick;
         container.Controls.Add (currentControl);
         previousControl = currentControl;
         CancelButton = currentControl;
      }
      {
         container.Height  = 0;
         closeActionsWidth = 0;
         foreach (Control control in container.Controls) {
            if (container.Height < control.Bottom) {
               container.Height = control.Bottom;
            }
            if (closeActionsWidth < control.Right) {
               closeActionsWidth = control.Right;
            }
         }
         container.Height  += Spacing;
         closeActionsWidth += Spacing;
      }
   }

   //==========================================================================
   protected void SaveAndCloseClick (Object sender, EventArgs e)
   {
      Save ();
      delayClose = false;
      Close ();
   }

   //==========================================================================
   protected void CloseOnlyClick (Object sender, EventArgs e)
   {
      delayClose = false;
      Close ();
   }
   //==========================================================================
   protected void DontCloseClick (Object sender, EventArgs e)
   {
      closeActions.Visible = false;
      ControlBox = true;
      //this.EnableCloseButton (true);
   }

   //==========================================================================
   protected void Save ()
   {
      // do save
   }

   //==========================================================================
   protected override void OnFormClosing (FormClosingEventArgs e)
   {
      base.OnFormClosing (e);
      if (delayClose) {
         e.Cancel = true;
         closeActions.Visible = true;
         ControlBox = false;
         //this.EnableCloseButton (false);
         ActiveControl = saveAndCloseButton;
         ClientSize = new Size (Math.Max (ClientSize.Width, closeActionsWidth),
                                Math.Max (ClientSize.Height, closeActions.Height));
         return;
      }
   }
}

////*****************************************************************************
//public static class WindowHelper
//{
//   //--------------------------------------------------------------------------
//   private const uint SC_CLOSE = 0xF060;
//
//   private const uint MF_ENABLED   = 0x00000000;
//   private const uint MF_GRAYED    = 0x00000001;
//   private const uint MF_DISABLED  = 0x00000002;
//
//   //==========================================================================
//   [DllImport ("user32.dll")]
//   static extern IntPtr GetSystemMenu (IntPtr hWnd, bool bRevert);
//
//   //==========================================================================
//   [DllImport ("user32.dll")]
//   static extern bool EnableMenuItem (IntPtr hMenu, uint uIdEnableItem, uint uEnable);
//
//   //==========================================================================
//   public static void EnableCloseButton (this Form form, bool bEnabled)
//   {
//      IntPtr hSystemMenu = GetSystemMenu (form.Handle, false);
//
//      EnableMenuItem (hSystemMenu, SC_CLOSE, (bEnabled ? MF_ENABLED : MF_GRAYED | MF_DISABLED));
//   }
//}

//*****************************************************************************
public static class App
{
   //==========================================================================
   public static void Main (string [] args)
   {
      Application.Run (new EditorForm ());
   }
}

Varianten:

Das Snippet zeigt das grundlegende Prinzip. Die konkrete Ausgestaltung bleibt euch überlassen. So ist die Hintergrundfarbe für das Panel natürlich frei wählbar, genauso wie der ButtonStyle (ich habe mich hier für Flat entschieden). Außerdem können die Beschriftungen der Buttons und deren Anordnung beliebig verändert werden, genauso wie die Aktionen, die sie durchführen. Weiterhin könnte man die Höhe des Forms beim Einblenden des Panels um dessen Höhe vergrößern und beim Ausblenden entsprechend verkleinern. Aktuell wird nur sichergestellt, dass das Form groß genug ist bzw. wird, damit der Inhalt des Panels komplett sichtbar ist.

Natürlich muss man die Buttons und das Panel nicht per handgeschriebenem Code erzeugen, sondern kann sie auch mit den Visual Studio Designer erstellen. Wenn man viele verschiedene Abfragen hat, kann es sinnvoll sein, das Panel und die Buttons erst bei Bedarf zu erzeugen und anschließend zu zerstören. Wenn das eigentliche Fenster nicht nur aus einem Control besteht (hier: RichTextBox), sondern aus mehreren, bietet es sich an, diese Controls auf ein zusätzliches Panel mit DockStyle.Fill zu packen.

Ich habe mit mir gerungen, ob ich - während das Panel angezeigt wird - die Buttons in der Titelleiste ausblenden soll (ControlBox = false) oder nicht. Der Nachteil ist, dass man das Fenster wegen des dann fehlenden Buttons nicht mehr minimieren kann, solange die Abfrage angezeigt wird, obwohl ein Minimieren an sich problemlos möglich wäre. Maximieren geht per Doppelklick auf die Titelleiste weiterhin. Trotzdem habe ich mich für das Ausblenden entschieden, weil ich während der Tests regelmäßig der Versuchung erlegen bin, das Fenster durch einen erneuten Klick auf das X endgültig zu schließen, was ja gerade nicht möglich ist und nicht möglich sein soll. Nach dem (vollständigen) Ausblenden lenkte sich meine Aufmerksamkeit von alleine auf die Abfrage. Natürlich könnte man den X Button auch nur ausgrauen (der Code auf Basis von Schließen-Button (X) von Form ausblenden oder ausgrauen ist auskommentiert im Snippet enthalten). Das war mir persönlich allerdings zu dezent. Aber experimentiert ruhig selbst damit.

[EDIT]Weitere Varianten werden in den folgenden Beiträgen vorgeschlagen.[/EDIT]

Wenn man solche Abfragen häufiger benutzen möchte, bietet es sich an, eine eigene Klasse dafür zu schreiben, der man dann z.B. nur noch die Anzahl und Beschriftung der Buttons übergeben muss und die per eigenem Event mitteilt, welcher Button geklickt wurde. Wer so eine Klasse schreibt, kann sie gerne hier veröffentlichen.

Schlagwörter: 1000 Worte, modal, nichtmodal, nicht-modal, modale, nichtmodale, nicht-modale MessageBox, MessageBoxen, Dialog, Dialoge, Abfrage, Abfragen, Sicherheitsabfrage, Sicherheitsabfragen, Security Query, Queries, Question, Questions, Confirmation Prompt, Message, Input, InputBox, InputBox, InputBoxen

Screenshot: (erstes Bild: normale Ansicht; zweites Bild: Ansicht, nachdem auf das X geklickt wurde)

B
357 Beiträge seit 2010
vor 11 Jahren

Danke für das Snippet. Sieht sehr gut aus und ist tatsächlich eine sehr gute Alternative für modale Dialoge. 😉

B
293 Beiträge seit 2008
vor 11 Jahren
Hinweis von herbivore vor 11 Jahren

Zur Info: Der folgende Code ändert - wenn das Panel bereits angezeigt und erneut auf Schließen (X) geklickt wird - für einen kurzen Moment die Hintergrundfarbe des Panels im schnellen Wechsel zwischen rot und blau, so dass es auffällig flackert.

Ich finde das ControlBox ausbleden eher ungünstig. Ich denke, dass das unnatürlich aussieht und die User verwirren könnte. Wenn du die Aufmerksamkeit auf das Panel lenken willst, warum dann nicht zum Beispiel so?


protected override void OnFormClosing(FormClosingEventArgs e)
    {
        base.OnFormClosing(e);
        if (delayClose && !closeActions.Visible)
        {
            e.Cancel = true;
            closeActions.Visible = true;
            //ControlBox = false;
            //this.EnableCloseButton (false);
            ActiveControl = saveAndCloseButton;
            ClientSize = new Size(Math.Max(ClientSize.Width, closeActionsWidth),
                                   Math.Max(ClientSize.Height, closeActions.Height));
            return;
        }
        else if (delayClose && closeActions.Visible)
        {
            e.Cancel = true;
            closeActions.Tag = 0;
            Timer tiBlink = new Timer();
            tiBlink.Interval = 50;
            tiBlink.Tick += new EventHandler(tiBlink_Tick);
            tiBlink.Start();
            return;
        }
    }

    void tiBlink_Tick(object sender, EventArgs e)
    {
        int state = ((int)closeActions.Tag);
        if (state < 12)
        {
            if (closeActions.BackColor != Color.LightCoral)
                closeActions.BackColor = Color.LightCoral;
            else
                closeActions.BackColor = Color.FromArgb(165, 201, 239);

            closeActions.Tag = (state + 1);
        }
        else
        {
            closeActions.BackColor = Color.FromArgb(165, 201, 239);
            (sender as Timer).Stop();
        }
    }

Wenn ich nicht hier bin, findest du mich auf code-bude.net.

herbivore Themenstarter:in
49.485 Beiträge seit 2005
vor 11 Jahren

Hallo blutiger_anfänger,

darüber, was (un)natürlich wirkt, kann man sicher geteilter Meinung sein. Deshalb habe ich zu eigenen Experimenten aufgefordert und als ein solches begrüße ich deinen Vorschlag. Jeder kann selbst entscheiden, welche Variante er wählt oder ob er eine weitere Variante implementieren will.

herbivore

C
2.121 Beiträge seit 2010
vor 11 Jahren

Hallo

Lob! Als jemand der kürzlich eine Diskussion darüber angestoßen hat, finde ich das wirklich interessant und cool dass du dir darüber Gedanken gemacht hast.

Wie man mit der ControlBox oder der "Modalität" des Panels umgeht, kann man ja selbst je nach Anforderung regeln. Obs ungewohnt wirkt oder in welcher Ausführung es praktikabel ist, hängt ja auch vom aktuellen Fall ab.

In meinem Fall, einem evtl. selten genutzen aber dafür für den Betrieb einflussreichem Formular würde ich den Hinweis über das ganze Formular einblenden damit der Benutzer auswählen müssen (praktisch modal) was er tun will. Das gibt klare Verhältnisse vor, zum Beispiel: ok ich arbeite jetzt doch noch weiter und nehme damit zur Kenntnis dass noch nichts gespeichert wurde und habe auch eine Reaktion auf den Schließen-Button bemerkt.
Sonst verliert man sich im weiterarbeiten, das Panel gerät in Vergessenheit und beim nächsten Klick aufs X passiert nichts mehr (Panel ist ja schon da). Anruf folgt 😃

Aber man hat ja alle Freiheiten, das Panel einzublenden. Und vor allem auch bei den Hinweistexten, die man viel beschreibender machen kann als eine MessageBox es kann.

herbivore Themenstarter:in
49.485 Beiträge seit 2005
vor 11 Jahren

Hallo chilic,

richtig, das Prinzip kann man nicht nur für Abfragen, sondern auch für Hinweistexte oder sogar Fehlermeldungen anwenden. (Ich habe jetzt einen entsprechenden Absatz in den Startbeitrag eingefügt.

Dass man weiterarbeiten kann, während das Panel angezeigt wird, halte ich - gerade bei Fehlermeldungen, aber auch sonst - für den entscheidenden Vorteil. Denn es ist mir schon oft passiert, dass ich eine Fehlermeldung weggeklickt habe (und musste), um das eigentliche Fenster zur Fehlerbehebung wieder bedienen zu können ... und dann nicht mehr (ausreichend genau) wusste, was in der MessageBox stand. Das eingeblendete Panel kann man dagegen solange stehen lassen, bis man den Fehler tatsächlich behoben hat. Das ist besonderes nützlich, wenn mehrere Schritte zur Fehlerbehebung nötig sind und diese im Meldungstext somit während der gesamten Fehlerbehebung angezeigt werden.

Aber selbst wenn man sich bei einer Abfrage entscheidet, das Panel mit DockStyle.Fill (statt DockStyle.Top) zu versehen und damit ein inhaltliches Weiterarbeiten mit dem Fenster praktisch unmöglich macht, bleibt wenigstens noch der Vorteil, dass das Fenster selbst frei verschiebbar bleibt. Trotzdem sehe ich diese Möglichkeit klar als die schlechtere Wahl. Aber natürlich kann das jeder selbst entscheiden.

Um die kleine Klippe zu umschiffen, dass das Panel von Benutzer nicht bemerkt oder über das Weiterarbeiten ganz vergessen wird, wurden mehrere Vorschläge gemacht und es gibt sicher noch weitere Möglichkeiten. Diesen Punkt bekommt man also sicher in den Griff. Wenn man ganz sicher gehen will, könnte man beim zweiten (oder n-ten) Klick auf den Schließen-(X)-Button, das Panel wieder ausblenden und eine normale MessageBox anzeigen. Quasi als Rückfallmöglichkeit für die Unverbesserlichen. 😃 Aber auch das ist nur ein Vorschlag und jeder muss für sich selbst die Vor- und Nachteile abwägen.

Natürlich stellt sich bei Neuerungen immer auch die Frage, wie die Benutzer damit klarkommen, ob sie die Vorteile für sich erkennen oder es dagegen als Verschlechterung ansehen. Aber das kann man auf der theoretischen Ebene nicht abschließend klären. Im Zweifel rechtfertigen die objektiv vorhandenen Vorteile bei gleichzeitig geringem Realisierungsaufwand mindestens ein Praxisexperiment. Meine Freundin, die sicher kein Computerfreak ist, kam jedenfalls ohne jegliche Erklärung (nur: "Klick mal aufs X") mit dem Panel auf Anhieb prima klar.

herbivore

L
416 Beiträge seit 2008
vor 11 Jahren

Hi,

verwende das Panel zur Zeit um Eingabeparameter aus dynamischen Vorlagen abzufragen. Weiß nicht ob das deine Intention war aber mir gefällts ganz gut.
Danke also auch von mir für die Vorlage!

1.815 Beiträge seit 2005
vor 10 Jahren

Hallo,

habe gerade noch einen anderen Vorteil entdeckt:
Wenn man die jeweiligen Inhalte für diesen Bereich als einzelne Controls "anbietet" (also dann auch ganz im Sinne von WPF), hat man für spätere Szenarien eine bessere Kontrolle, diese Inhalte rel. schnell in anderen Controls (Forms, tabPages, ...) zu verwenden.

edit: Ergänzung, um die Aussage etwas klarer zu machen.
Wenn man z.B. eine Liste mit zentralen Controls verwaltet, oder einen InjectionContainer, oder einfach nur zentrale Methoden, welche einem ein für einen bestimmten zweck geeignetes Control zurückgeben (z.B. Eingabe der Zugangsdaten für eine SQL-verbindung), dann kann man hier relativ schnell umschalten zwischen modalen Dialogen oder dem von herbivore vorgeschlagenen Weg, da in beiden Fällen einfach nur die zentrale Methode, Liste, ... abgerufen und das Control eingebunden werden muss.

Nobody is perfect. I'm sad, i'm not nobody 🙁