Laden...

IDataErrorInfo im typisierten Dataset

Erstellt von ErfinderDesRades vor 15 Jahren Letzter Beitrag vor 15 Jahren 10.825 Views
ErfinderDesRades Themenstarter:in
5.299 Beiträge seit 2008
vor 15 Jahren
IDataErrorInfo im typisierten Dataset

Hallo liebe Leuts!

Das IDataErrorInfo ist so: Delegates and Business Objects (codeProject) oder so: Customisable ErrorProviders (codeProject) eine ausnehmend fabelhafte Sache.
Man kann sehr differenzierte Validierungen unternehmen, mit einem Minimum an Code.
Dabei kann man die eigentlichen Überprüfungen komplett unabhängig vom Gui implementieren, und es der Gui-Implementation überlassen, wie sie auf ungültige Eingaben reagieren will.

Eine weitere Erfreulichkeit ist, daß das Datagridview einen ErrorProvider _fixnfertig eingebaut _hat, der mit diesem Interface arbeitet.
Also bei Fehleingaben gibts den kleinen Icon direkt in der fehlerhaften Zelle, und im Tooltip die Fehlerbeschreibung.
Und zwar ohne jedes Zutun des Programmierers, sofern im Business-Layer IDataErrorInfo richtig implementiert ist.

Die obigen Links arbeiten mit ordentlichen Datenklassen, und ich wollte wissen, ob das auch mit typisierten Datasets geht.
Es geht, auch ebenso elegant, nur eben ganz anders.
Bei einer gebundenen DataTable verwaltet die BindingSource ja keine DataRows, sondern spezielle Wrapper derselben, die DataRowViews.
Und die haben IDataErrorInfo schon vor-implementiert.
Wo soll man nun die Validierungen hinschreiben, wenn IDataErrorInfo schon weg ist?
Man muß halt das RowChanging-Event der zugrundeliegenden DataTable behandeln, und dort einen "RowError" setzen (den das DataRowView dann intern abruft).
Die folgende kleine Validierung checkt schon mal für 2 Spalten je sowohl Null-Eingabe als auch je eine spaltenspezifische Gültigkeit.


 void CustomerDataTable_RowChanging(object sender, System.Data.DataRowChangeEventArgs e) {
    if (!_ObservedActions.Contains(e.Action )) return;
    CustomerRow rw = (CustomerRow)e.Row;
    if (rw.HasErrors) rw.ClearErrors();
    if (rw.IsCustomerNameNull()) rw.SetColumnError("CustomerName", "please input Name!");
    else if (rw.CustomerName.Length < 4) rw.SetColumnError("CustomerName", "Name too short!");
    if (!rw.IsCustomerPhoneNull() && !_PhoneRgx.IsMatch(rw.CustomerPhone)) {
       //obige Bedingung greift, wenn nichtNull eingegeben wurde, aber der Regex scheitert
       rw.SetColumnError("CustomerPhone", "Phone not valid!");
    }
 }

Der frühe Apfel fängt den Wurm.

35 Beiträge seit 2008
vor 15 Jahren

hey raderfinder!

also ich hab den codeschnipsel bei mir mal reingehauen,
aber wenn ich in eine schon vorhandene oder neue zelle einen ungültigen wert eingebe, dann kommt da gar nix denn das grid bricht ab und setzt den alten wert wieder ein bzw. entfernt den neu angefangenen datensatz gleich wieder.

hier mein code:

void maschinen_RowChanging(object sender, DataRowChangeEventArgs e) {
            DSKruseBsat.maschinenRow row = e.Row as DSKruseBsat.maschinenRow;
            if (row.HasErrors) row.ClearErrors();
            if (String.IsNullOrEmpty(row.Name_Ger)) row.SetColumnError("Name_Ger", "Name darf nicht leer sein!");
        }

ich nehme an ich muss das datagrid noch irgendwie auf die fehleranzeige einstellen?
hätte gerne so eine kleine übersicht was ich wo noch einstellen muss damit die kleinen roten fehlerbuttons erscheinen 😃

und ist nicht "row_changing"das falsche event fürs behandeln der fehler beim neueinfügen? .. unsicher!

danke,
D

ErfinderDesRades Themenstarter:in
5.299 Beiträge seit 2008
vor 15 Jahren

ich nehme an ich muss das datagrid noch irgendwie auf die fehleranzeige einstellen? nicht dassich wüsste
und ist nicht "row_changing"das falsche event fürs behandeln der fehler beim neueinfügen? .. unsicher!

Beim neueinfügen ist e.Action == DataRowAction.Add

Wird das event bei dir überhaupt aufgerufen?

Der frühe Apfel fängt den Wurm.

ErfinderDesRades Themenstarter:in
5.299 Beiträge seit 2008
vor 15 Jahren
Update 19.1.09

Das 2. Form, das mit der Bindung des Grids an eine BindingList<BusinessObject>, das funzte ja gar nicht!
Warum sagtnmir das keiner?
Jedenfalls habe ich jetzt IDataErrorInfo glaub noch gründlicher verstanden, gugge diese Implementation, da isses versucht zu erläutern.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Windows.Forms;
using System.Linq;

namespace TestDataErrorInfo {
   public class GridRowData : IDataErrorInfo {
      //anzumerken, dass dieses Business-Object eine Katastrophe ist: INotifyPropertyChanged ist nicht
      // implementiert, und ordentlicherweise gehört Col1 und Col3 als numerischer Datentyp angelegt,
      // wenn die Regeln eine numerische Eingabe verlangen.

      public string Col1 { get; set; }
      public string Col2 { get; set; }
      public string Col3 { get; set; }
      public string Col4 { get; set; }

      //gewissermaßen nach dem Flyweight-Pattern: ein Dictionary für alle Datensätze, und kommt
      // nur was rein, wenn tatsächlich Fehler vorhanden.
      private static Dictionary<GridRowData, Dictionary<string, string>> _Errors = new Dictionary<GridRowData, Dictionary<string, string>>();

      private static Dictionary<string, string> _ErrorsTemp = new Dictionary<string, string>();

      #region IDataErrorInfo Members
      //Das Interface funzt so: Erst wird für den ganzen Datensatz IDataErrorInfo.Error abgerufen.
      // Hier wird der Datensatz validiert, ggfs. auch die verschiedenen Spalten gegeneinander.
      // Für jede Spalte kann ein Fehler gemerkt werden, und eine Art Summary (wie immer sie
      // gebildet wird) wird zurückgegeben.
      //Die "Einzelfehler" der Spalten werden anschließend nochmal einzeln je Spalte (über
      // IDataErrorInfo.this[string columnName]) abgerufen.
      //etwas unperformant (?): die "Einzelfehler" werden auch dann abgerufen, wenn die Summary
      // (IDataErrorInfo.Error)  null oder "" zurückgibt.

      string IDataErrorInfo.Error {
         //die Regel: Col1 muß zahl sein, kleiner als Col3, ist recht komplex. Besonders tricky,
         // daß ein Eintrag in die eine Spalte auch zu einem Fehler in der anderen führen kann.
         get {
            string ret = "";
            int value1;
            var isInt1 = int.TryParse(Col1, out value1);
            if (!isInt1) { _ErrorsTemp["Col1"] = "muß Zahl sein"; }
            else if (value1 < 0) { _ErrorsTemp["Col1"] = "muß >= 0 sein"; }
            int value3;
            var isInt3 = int.TryParse(Col3, out value3);
            if (!isInt3) { _ErrorsTemp["Col3"] = "muß Zahl sein"; }
            else if (isInt1 && value3 <= value1) {
               _ErrorsTemp["Col1"] = "muß < Col3 sein";
               _ErrorsTemp["Col3"] = "muß > Col1 sein";
            }
            if (_ErrorsTemp.Count > 0) {
               _Errors[this] = _ErrorsTemp;
               //Summary aller Errors bilden
               Func<KeyValuePair<string, string>, string> buildSummarySegment =
                  kvp => string.Concat(kvp.Key, " ", kvp.Value);
               ret = string.Join(", ", _ErrorsTemp.Select(kvp=>buildSummarySegment(kvp)).ToArray());
               //neues _ErrorsTemp bereitstellen
               _ErrorsTemp = new Dictionary<string, string>();
            }
            else _Errors.Remove(this);
            return ret;
         }
      }

      string IDataErrorInfo.this[string columnName] {
         get {
            Dictionary<string, string> errDic;
            //nur wenn für dieses Datenobjekt überhaupt Fehler anliegen...
            if (_Errors.TryGetValue(this, out errDic)) {
               string err;
               //... gucken, ob für diese Spalte ein Eintrag.
               if (errDic.TryGetValue(columnName, out err)) return err;
            }
            return "";
         }
      }

      #endregion
   }
}


(Das Update im Orig.-Post)

Der frühe Apfel fängt den Wurm.

F
10.010 Beiträge seit 2004
vor 15 Jahren

Bei deinem Beispiel sieht man schön, wie unübersichtlich und schlecht
wiederverwendbar so etwas gemacht werden kann.

1.
Businessentities sollten immer in einem Validen zustand sein, nicht erst nach einem
Abfragen der Validität.

2.
Das Abfragen aller ValidationRules in IDataErrorInfo.Error kann gerade bei
einer Auflistung zu massiven Performance Problemen führen.

3.
Sollten BusinessEntities auch INotifyPropertyChanged implementieren,
und hier kann bereits die Validierung für das eine Property geschehen,
am besten mit einer Rules basierten Lib.

4.
Diese Rulesengine ( z.b. aus CSLA ) macht es viel übersichtlicher.

5.
Implementiert die DataTable bereits IDataErrorInfo und du musst nur noch
die entsprechenden Members setzen.
Du sparst dadurch erheblich Speicher ein.

ErfinderDesRades Themenstarter:in
5.299 Beiträge seit 2008
vor 15 Jahren

Bei deinem Beispiel sieht man schön, wie unübersichtlich und schlecht
wiederverwendbar so etwas gemacht werden kann. Es kommt noch schlimmer 😉:


   public class GridRowData : DataErrorInfo<GridRowData> {
      //anzumerken, dass dieses Business-Object eine Katastrophe ist: INotifyPropertyChanged ist nicht
      // implementiert, und ordentlicherweise gehört Col1 und Col3 als numerischer Datentyp angelegt,
      // wenn die Regeln eine numerische Eingabe verlangen.

      public string Col1 { get; set; }
      public string Col2 { get; set; }
      public string Col3 { get; set; }
      public string Col4 { get; set; }

      protected override void Validate(ErrorInfo errors, ref string summary) {
         int value1;
         var isInt1 = int.TryParse(Col1, out value1);
         if (!isInt1) errors["Col1"] = "muß Zahl sein";
         else if (value1 < 0) errors["Col1"] = "muß >= 0 sein";
         int value3;
         var isInt3 = int.TryParse(Col3, out value3);
         if (!isInt3) errors["Col3"] = "muß Zahl sein";
         else if (isInt1 && value3 <= value1) {
            //hier erzeugt eine Regelüberprüfung 2 Fehler-Einträge
            errors["Col1"] = "muß < Col3 sein";
            errors["Col3"] = "muß > Col1 sein";
         }
         if (errors.Count > 0) {
            //Summary aller Errors bilden - hier: alle Meldungen aneinanderhängen
            //(alternativ könnte man auch die Anzahl ausgeben, oder auch ganz weglassen)
            summary = ", ".Between(errors.GetList().Select(kvp => " ".Between(kvp.Key, kvp.Value)));
         }
      }
   }

Hier habe ich das Interface in eine Basisklasse gepackt, und alles was an Validierung zu erledigen ist, ist in Validate() zu erledigen

1.
Hab ich auch gedacht.
Bis ich diesen hier gelesen hab. "CSLA" wird dort übrigens auch erwähnt, aber's scheint, im eben hier vorgestellten Sinne: In Rocky's CSLA framework, a business object won't usually throw exceptions if you set a property to an invalid value, but instead the object will mark itself as invalid. If you try to attempt to save the business object, then an exception might be thrown.

2.
(Dann solltest du mal die Lösung angucken, die Paul Stovell im zitierten Artikel vorstellt.)
Ich bin halt noch am rumprobieren, und ob das schlechte Performance bringt, bliebe auszuprobieren. Evtl. wird dieses Interface ja nur angefragt, wenn Entities dargestellt werden sollen. In dem Fall würden während eines Komplett-Zeichenvorganges eines DGVs maximal 60 Entities durchgecheckt, denn mehr passen kaum auf einen Bildschirm.

3.
Das habich ja im Kommentar angemerkt, daß INotifyPropertyChanged implementiert gehört.
Und ich sehe auch ein Problem bei diesem Ansatz (wie soll ich ihn nennen - Rocky-Ansatz? CSLA-Ansatz? Stovell-Ansatz?):
Nicht alle Eingaben _können _überhaupt entgegengenommen werden. Wird einem Integer-Feld ein String zugewiesen, muß das BO gleich ablehnen, es kann nicht akzeptieren, und sich als ungültig markieren.
Daraus folgt also ValidierungsCode an 2 verschiedenen Stellen, mit 2 verschiedenen Auswirkungen.
Andererseits scheinen mir gewisse Regeln, die mehrere Properties eines BO einbeziehen ("Col3 soll größer als Col1 sein") es nahezulegen, daß das BO die Eingaben erstmal entgegennimmt, sich aber als invalid markiert.
Das macht die Eingabe auch sehr komfortabel: Der Errorprovider zeigt an, wos klemmt, und du kannst den Fehler sowohl in Col1 als auch in Col3 ausbügeln.

4.
Ja, werdichmir mal angucken - kannst du vllt. ein kleines Beispiel machen, wie die hier gezeigten Rules für CSLA zu codieren wären?

5.
Das ist nicht die DataTable, die das implementiert, sondern es sind die DataRowViews, die vonne BindingSource verwaltet werden.
Dazu ist ein anderes Beispiel in derselben Solution. Darauf gehe ich in meinem allerersten Post ein. Und im dort geposteten Code tu ich genau das: die entsprechenden Members setzen
Zum Speicherverbrauch bei den BOs: Den habich auf möglichst niedrig optimiert. Die IDataErrorInfo implementierende Basis-Klasse hält nur ein Feld, und das ist nur dann Nicht-Null, wenn Fehler anzuzeigen sind.

Also hier mein neues Machwerk:

Der frühe Apfel fängt den Wurm.

ErfinderDesRades Themenstarter:in
5.299 Beiträge seit 2008
vor 15 Jahren

Hier zeigichmal, was das DGV damit anstellt: Jede fehlerhafte Zelle kriegt einen Erroprovider, und die "Summary" wird im Rowheader errorprovidert:

Der frühe Apfel fängt den Wurm.

ErfinderDesRades Themenstarter:in
5.299 Beiträge seit 2008
vor 15 Jahren

Und hier noch der Tooltip der "Summary"

Der frühe Apfel fängt den Wurm.

F
10.010 Beiträge seit 2004
vor 15 Jahren

Habe deine Demo mal mit "meinen" Rules versehen und auch ein bischen umgebaut.

GridRowData sieht jetzt so aus:


public class GridRowData : DataErrorInfo<GridRowData>
    {
        #region --- Local variables ---
        private int _Col1;
        private string _Col2;
        private int _Col3;
        private string _Col4;
        #endregion        
        #region --- Properties ---
        public int Col1 
        {
            get { return _Col1; }
            set 
            { 
                CheckPropertyChanged<int>("Col1", ref _Col1, ref value);
                DependingProperty("Col3");
            }
        }
        public string Col2 
        {
            get { return _Col2; }
            set { CheckPropertyChanged<string>("Col2", ref _Col2, ref value); }
        }
        public int Col3 
        {
            get { return _Col3; }
            set 
            { 
                CheckPropertyChanged<int>("Col3", ref _Col3, ref value);
                DependingProperty("Col1");
            } 
        }
        public string Col4 
        {
            get { return _Col4; }
            set { CheckPropertyChanged<string>("Col4", ref _Col4, ref value); }
        }
        #endregion
        #region --- AddValidation ---
        protected override void AddValidationRules()
        {
            base.AddValidationRules();
            ValidationRules.AddRule(CommonRules.GreaterThanOrEqualToValue<int>, 
                new CommonRules.CompareValueRuleArgs<int>("Col1", 0));
            ValidationRules.AddRule(CommonRules.PredicateRule<int>, 
                new CommonRules.PredicateRuleArgs<int>("Col1", x => x < _Col3, "muß < Col3 sein"));
            ValidationRules.AddRule(CommonRules.PredicateRule<int>, 
                new CommonRules.PredicateRuleArgs<int>("Col3", x => x > _Col1, "muß > Col1 sein"));
        }
        #endregion
    }

Der Rest geschieht in DataErrorInfo<T>.

ErfinderDesRades Themenstarter:in
5.299 Beiträge seit 2008
vor 15 Jahren
Wow

Jau, da hastemich im wesentlichen überzeugt.
Das Teil ist wirklich so designed, daß die Validierung schon im Property-Setter stattfindet, und auch nur die angegebenen Regeln validiert.
Die IDataError-Interface-Member holen nur noch die Ergebnisse ab (wo bei mir ja bei jedem Errors Abruf neu validiert wurde).
Sehr schick auch, mit DependingProperty("Col3"); abhängige Properties explizit angeben zu können (wo ich ja immer alle Props der Entity durchchecke).

Beim Speicherbedarf sehe ich evtl. noch ziemliches Spar-Potential:
Ich bin nämlich noch nicht überzeugt, daß jede Entity ihre _eigene _ValidationRules-Auflistung braucht, mit mehr o. weniger aufwändigen Rule-Objekten drin, vermutlich minimal für jede Property eines.
Diese Rules sind doch für jede Entity dieselben, das müsste man doch auch über eine statische ValidationRules-Auflistung gebacken kriegen.
Sodaß in der Entity wirklich nur noch ggfs.(!) die Fehler gemerkt werden.

Aber wie das umsetzen, da werde ich noch eine Weile dran rumwursteln.

Von den vorgefertigten Rules binnich auch nicht so rasend überzeugt - ich weiß nicht, ob ich statt


            ValidationRules.AddRule(CommonRules.PredicateRule<int>,
                new CommonRules.PredicateRuleArgs<int>("Col1", x => x < _Col3, "muß < Col3 sein"));

nicht doch lieber schriebe:


             if (Col1 >= Col3) errors["Col1"] = "muß < Col3 sein";

Aber vllt. läßt sich erstere Variante auch vereinfachen.
Mir scheint, wennich ein CommonRules.PredicateRuleArgs<int> angebe, kommt als Handler eh nix anneres als die CommonRules.PredicateRule<int> in Frage - das könnte sich die ValidationRules - Auflistung also ggfs. selber denken, denke ich.

Und statt PredicateRules könnte ich mir so Func<bool> - Rules vorstellen, also dasses letzlich auf sone Form hinausläuft:


         ValidationRules.AddRule("Col1", "muß < Col3 sein", () => _Col1 < _Col3);

Edit: Vielen Dank, überhaupt, das hat mich jetzt wirklichn Stück schlauer gemacht 🙂

Der frühe Apfel fängt den Wurm.

F
10.010 Beiträge seit 2004
vor 15 Jahren

Static ist immer ein Problem.
Was ist z.b., wenn Du in der UI zusätliche Rules benötigst, die z.b.
gegen eine DB validieren?

Wenn du dagegen ein AddRules in deinem BO hast, kannst du jederzeit
auf ein einzelnes Objekt zusätzliche Rules anwenden.

Und sicher kann man bei den Rules auch für jede Rule ein eigenes Add
machen, aber so hast Du einen standart, erst die Funktion, die eine Rule
ausführt, dann die Parameter.
Das kann man dann ganz Simple auch mit einem Designer erledigen.
Schau dir dazu auch mal die Validations aus der EnterpriseLib an.

http://www.codeproject.com/KB/dotnet/vapppblock.aspx

49.485 Beiträge seit 2005
vor 15 Jahren

Hallo ihr beiden,

solange ihr auf hohem Niveau diskutiert, in der Sache weiter kommt und euch nicht im Kreis dreht und das Ergebnis den Benutzern des Snippets dient, ist es ok. Aber verliert euch bitte nicht in der Diskussion. Wir sind hier ja in ".NET-Komponenten und C#-Snippets".

herbivore

35 Beiträge seit 2008
vor 15 Jahren

ich hab nochwas dazu im thread DataGridView zellvalidierung & fehlerbehandlung geschrieben..