Laden...

[Artikel] Draggen innerhalb der Anwendung - sicher und benutzerfreundlich

Erstellt von ErfinderDesRades vor 14 Jahren Letzter Beitrag vor 14 Jahren 20.452 Views
ErfinderDesRades Themenstarter:in
5.299 Beiträge seit 2008
vor 14 Jahren
[Artikel] Draggen innerhalb der Anwendung - sicher und benutzerfreundlich

Dieser Artikel beschränkt sich auf Draggen innerhalb einer Anwendung. Hier gelten spezifische Bedingungen, und wenn man dem Rechnung trägt, kann man Drag&Drop einfacher und sicherer implementieren, als wenn man sich von der Framework-Unterstützung für Drag&Drop leiten läßt. Die Framework-Unterstützung ist nämlich auf anwendungsübergreifendes Drag&Drop ausgelegt.

übliche Vorgehensweise: 1.Das Quell-Control verarbeitet die Events Quell_MouseDown() und Quell_MouseMove(), um den User-Input "Dieses soll gezogen werden" zu identifizieren.
Ist ein solcher Input identifiziert, wird ein DataObject mit den zu ziehenden Daten befüllt, und mit Quell.DodragDrop() wird der DragVorgang dann gestartet (unter Angabe der zulässigen DropEffekts (Kombinationen möglich aus "Move", "Copy", "Link", "Scroll")). 1.Das Ziel-Control muß das Ziel_DragOver()-Event verarbeiten, und ggfs. unter Berücksichtigung der Modifizierer-Tasten den speziellen DragDropEffekt festlegen. Geschieht dieses nicht, bleibt er auf DropEffekts.None, und auf dem Ziel kann nicht gedropt werden.
Alternativ kann das Ziel auch _DragEnter() verarbeiten, aber _DragOver() hat den Vorzug, dass während des Ziehens noch mittels Modifizier-Tasten (Shift, Strg, Alt) der gewünschte DropEffekt modifiziert werden kann. 1.Im Ziel_DragDrop()-Event ist die Eingabe beendet, und die vom User gemeinte Aktion wird umgesetzt.

**Zwei erhebliche Mängel dieser Vorgehensweise:**1.:::

Ursache dieser Eigenheit ist die anwendungsübergreifende Ausrichtung: Der Drag-Vorgang wird an einen OLE-Mechanismus weitergegeben, und die Events kommen dann vom System. Der OLE-Mechanismus unterdrückt alle Exceptions, da sonst auch DragOver-Fehler von eigentlich unbeteiligten Dritt-Anwendungen einen Drag-Vorgang stören würden, wenn zufällig über die Dritt-Anwendung gezogen wird.
Für das _DragOver()-Event folgt daraus: Den Code möglichst einfach halten. Da das Programm einfach weiter läuft, verursachen selbst einfachste Fehler stundenlanges Suchen.
Noch ungünstiger wirkt sich diese Eigenheit im _DragDrop()-Event aus, wenn die gemeinte Aktion umgesetzt werden soll. Hier kann die gesamte Anwendung betroffen sein, und ein Fehler auch Datenbestände beschädigen. Auch werden Methoden aufgerufen, die evtl. später noch weiterentwickelt werden, sodaß ein ursprünglich funktionierendes Dragging sich auf einmal fehlerhaft verhält (wie gesagt: ohne jede Rückmeldung!). Hier ohne Debug-Unterstützung der IDE zu programmieren ist ziemlich riskant.
Edit März 2012: Die haben den Bug behoben! VisualStudio2010 und die .Net4-Runtime behandeln nun auch Fehler innerhalb von Drag&Drop-Events korrekt, also mit CodeStop und pipapo, und ermöglichen somit eine gezielte Fehlersuche. 1.Das DataObject ist überflüssig und verleitet zu unsicheren Implementationen, bei denen unzulässige Daten aus anderen Anwendungen auf die eigene gezogen werden können.

**Lösung: Draggen ohne DataObject und _DragDrop-Event ** 😃

Das ist sogar einfacher, weil ein Event entfällt, und der Umgang mit dem DataObject. Ein DataObject kann mit beliebigen Daten befüllt werden, dementsprechend umständlich und fehleranfällig ist seine Handhabung. Dabei sind statt amorpher Daten genau 4 typsichere Informationen erforderlich, um den Zustand jedes DragVorgangs fehlerfrei zu bewerten:1.das Quell-Control 1.die Position der Maus über dem Quell-Control, zum Zeitpunkt des DragStarts 1.das Ziel-Control 1.die Position der Maus über dem Ziel-Control, zum Zeitpunkt des DragOvers/DragDrops

Diese Informationen mussten auch bei der herkömmlichen Vorgehensweise ermittelt werden: Die Quell-Control-Infos brauchte man, um das Quell-Item zu ermitteln, anhand dessen das DataObject befüllt wurde, die Ziel-Control-Infos, um festzulegen, wo eingefügt wird.
Das _DragDrop-Event kann einfach ausgelassen werden - der endgültige DropEffekt wird ebensogut von Control.DoDragDrop() zurückgegeben. Dadurch verlagert sich die Umsetzung der gemeinten Aktion in das _MouseMove() des Quell-Controls, was recht praktisch ist, weil so hat man die Quell-Control-Infos gleich zugreifbar.
Vor allem aber hat man die Debug-Unterstützung wieder, bei der Entwicklung der umzusetzenden Aktion.

Benutzer-freundlichkeit

Dazu gehört m.E. wesentlich eine Markierung des Drop-Zieles, schon im DragOver. Um dem User Feedback zu geben, wo sein Drag-Item droppen wird.
Wird hier nicht weiter besprochen, ist im Sample-Code dabei.

Im Download enthaltener Code enthält auch eine recht vollständige Lösung für Treeview, ListView, Listbox und DatagridView, aber in der Praxis mag man es evtl. lieber kleiner und überschaubarer haben:


using System;
using System.Drawing;
using System.Windows.Forms;

namespace BaseDrag {

   //Form mit 3 Listboxen mit (im Designer zugefügten) Items und .AlloDrop=true.
   public partial class frmBaseDrag : Form {

      // Draggen innerhalb einer Anwendung ist ohne DataObject einfacher und sicherer
      // Aber Control.DoDragDrop() verlangt eines
      private static DataObject _dumData = new DataObject();

      // im Grunde kann bei .DoDragDrop() nur AllEffects angegeben werden, denn beim Drag-Start kann
      // der AllowedEffekt nicht eingeschränkt werden: Derselbe Ziehvorgang kann auf dem einen
      // ZielControl ausschließlich .Copy erfordern, und auf dem anderen ausschließlich .Move.
      // Es gibt zwar einen DragDropEffects.All, aber wenn man den verwendet, kann 
      // DragDropEffects.Link nicht eingestellt werden.
      private static DragDropEffects _allEffects = 
         DragDropEffects.Move | DragDropEffects.Copy | DragDropEffects.Link | DragDropEffects.Scroll;

      private ListBox _src;
      private ListBox _dest;

      public frmBaseDrag() {
         InitializeComponent();
         this.Location = Screen.PrimaryScreen.WorkingArea.Location;
         foreach (var lb in new ListBox[] { listBox1, listBox2, listBox3 }) {
            lb.MouseMove += ListBox_MouseMove;
            lb.DragOver += ListBox_DragOver;
            lb.DragLeave += (s, e) => Highlighter.Off();
         }
      }

      // ggfs. Dragvorgang starten, **und** Ergebnis umsetzen (Das _Drop-Event ist für die Umsetzung der
      // gemeinten Drag-Aktion ungeeignet, da auftretende Fehler nicht gemeldet werden.)
      private void ListBox_MouseMove(object sender, MouseEventArgs e) {
         if (e.Button != MouseButtons.Left) return;
         _src = (ListBox)sender;
         if (_src.SelectedIndex < 0) return;
         try {
            var effect = _src.DoDragDrop(_dumData, _allEffects);
            if (effect == DragDropEffects.None) return;
            var iDest = _dest.IndexFromPoint(_dest.PointToClient(Control.MousePosition));
            if (iDest < 0) iDest = _dest.Items.Count;
            switch (effect) {
               case DragDropEffects.Move:
                  _dest.Items.Insert(iDest, _src.SelectedItem);
                  _src.Items.RemoveAt(_src.SelectedIndex);
                  break;
               case DragDropEffects.Copy:
                  _dest.Items.Insert(iDest, _src.SelectedItem);
                  break;
               case DragDropEffects.Link:
                  _dest.Items.Insert(iDest, string.Concat("linked with: ", _src.SelectedItem));
                  break;
            }
         } finally {
            // Aufräum-Arbeiten
            Highlighter.Off();
            _src = null;
         }
      }


      // Hier wird der DropEffect gesetzt. 
      private void ListBox_DragOver(object sender, DragEventArgs e) {
         if (_src == null) return;         // Dragvorgänge fremder Anwendungen ablehnen
         try {
            _dest = (ListBox)sender;
            var pt = new Point(e.X, e.Y);
            var iDest = _dest.IndexFromPoint(_dest.PointToClient(pt));
            //testweise kann die folgende Zeile einmal "vergessen" werden, und der TryCatch entfernt.
            //Dann kann nicht mehr hinter das letzte Item gedroppt werden - aber eine Fehlermeldung 
            //gibt es nicht.
            if (iDest < 0) iDest = _dest.Items.Count;
            switch (Control.ModifierKeys) {
               case Keys.None:
                  e.Effect = DragDropEffects.Move;
                  if (_src == _dest) {
                     //bei "selfDrag" mit Effect.Move nicht vor oder hinter sich selbst ablegen
                     var diff = iDest - _src.SelectedIndex;
                     if (diff == 0 || diff == 1) e.Effect = DragDropEffects.None;
                  }
                  break;
               case Keys.Shift:
                  e.Effect = DragDropEffects.Copy;
                  break;
               case Keys.Control:
                  e.Effect = DragDropEffects.Link;
                  break;
               default:
                  e.Effect = DragDropEffects.None;
                  break;
            }
            if (e.Effect == DragDropEffects.None) return;
            //Zum Setzen des Effects rechne ich auch das Highlighten des Ziel-Items
            if (_dest.Items.Count == 0) {
               Highlighter.FullWidth(_dest, 0, _dest.ItemHeight);
            } else if (iDest == _dest.Items.Count) {
               Highlighter.After(_dest, _dest.GetItemRectangle(_dest.Items.Count - 1));
            } else {
               Highlighter.Before(_dest, _dest.GetItemRectangle(iDest));
            }
         } catch (Exception ex) {
            var sError = ex.ToString();
            // Da die IDE Fehler innerhalb von Dragging nicht fängt, hier ein gecodeter Codestop.
            // Die gelbe Markierung kann per Kontextmenu an den Anfang des TryCatches gesetzt werden,
            // und der Vorgang in Einzelschritten wiederholt. (Oder gleich sError nachlesen.)
            System.Diagnostics.Debugger.Break();
         }
      }

   }
}


Einfacher geht es leider nicht, will man mehrere DropEffects unterstützen, Debug-Unterstützung der IDE, und die Ziel-Item highlighten.
Draggen mit anderen Controls ist prinzipiell gleich, nur das Ermitteln des Items unter der Maus ist bei Treeview, ListView, Listbox und DatagridView je spezifisch.
Und bei Treeview ist die "SelfDrag mit Effect.Move"-Regel anders: Ein Node darf weder in sich selbst, seine ChildNodes noch seinen Parent abgelegt werden.

Straight-forward dagegen die Lösung mit Unterstützung der (beiliegenden) DragDropper-Klasse:


using System;
using System.Windows.Forms;
using DragDrop;

namespace BaseDrag {

   public partial class frmDragDropper : Form {

      public frmDragDropper() {
         InitializeComponent();
         var boxes = new ListBox[] { listBox1, listBox2, listBox3 };
         foreach (var b in boxes) {
            var dropper = new DragDropper(b);
            foreach (var bx in boxes) {
               dropper.AddJob(bx, DragDropEffects.Move, DragDropEffects.Copy, DragDropEffects.Link);
            }
            dropper.Drop += dropper_Drop;
         }
      }

      void dropper_Drop(object sender, DragDropper.DropEventArgs e) {
         var src = (ListBox)e.Origin.Control;
         var srcItem = src.Items[e.Origin.Index].ToString();
         var dest = (ListBox)e.Target.Control;
         switch (e.Effect) {
            case DragDropEffects.Copy:
               dest.Items.Insert(e.Target.Index, srcItem);
               break;
            case DragDropEffects.Move:
               dest.Items.Insert(e.Target.Index, srcItem);
               src.Items.RemoveAt(e.Origin.Index);
               break;
            case DragDropEffects.Link:
               dest.Items.Insert(e.Target.Index, "linked with: " + srcItem);
               break;
         }
      }
   }
}


Hier ist DragOver für die verschiedenen Controls schon implementiert und weggekapselt, sodaß man sich ganz auf die Umsetzung der gemeinten Aktion konzentrieren kann.
(Entsprechend aufwändig aber auch die DragDropper-Klasse)

Absicherung bei anwendungsübergreifendem Drag & Drop

Hier macht die Verwendung des DataObjects natürlich Sinn. Es besteht aber ebenfalls das Problem der fehlenden IDE-Debug-Unterstützung.
Man kann im Drop()-Event eine Verzögerung einbauen, sodaß der eigentliche Code erst ausgeführt wird, nachdem das Drop-Event abgelaufen ist. Hierzu kann das Application.Idle() - Event verwendet werden - Variante mit anonymen Methoden:


      void listBox1_DragDrop(object sender, DragEventArgs e) {
         // (ExecDrop() scheitert, wenn zu kurze Dateinamen gedropt werden.)
         Action ExecDrop = () => {
            var leftCut = Application.StartupPath.Length;
            foreach (string s in ((DataObject)e.Data).GetFileDropList()) {
               listBox1.Items.Add(e.Effect.ToString() + " " + s.Substring(leftCut));
            }
         };
         //ExecDrop();           // ohne Fehlermeldung
         AppDelay(ExecDrop);      // mit Fehlermeldung
      }

      public void AppDelay(Action action) {
         //erzeugt und registriert einen anonymen Eventhandler, der sich selbst wieder deregistriert.
         EventHandler idle = null;
         idle = (s, e2) => {
            Application.Idle -= idle;
            action();
         };
         Application.Idle += idle;
      }

Zum Schluß noch ein_ Dankeschön!_ an die PowerUser, die mich mit Gegenlesen und Kommentaren ein Stück weiter gebracht haben.

Der frühe Apfel fängt den Wurm.