Laden...

Inhalte von Textboxen erst nach Bestätigen durch Button via Databinding aktualisieren

Erstellt von brocel1 vor 8 Jahren Letzter Beitrag vor 8 Jahren 3.379 Views
B
brocel1 Themenstarter:in
9 Beiträge seit 2015
vor 8 Jahren
Inhalte von Textboxen erst nach Bestätigen durch Button via Databinding aktualisieren

Hallo Forummitglieder,

derzeit programmiere ich in c# und Win Forms und habe ein Problem. Und zwar folgendes:

Ich habe ein Main-Form, über das sich ein weiteres Form über den Button "Konfiguration" öffnen lässt. Dieses besteht aus Labels und Textboxen besteht. Jeder Textbox ist an einem Objekt gebunden:

 userTextEdit.DataBindings.Add(new Binding("Text",Settings.Instance,"UserName",true,DataSourceUpdateMode.OnPropertyChanged));

Das Objekt Settings.Instance besitzt verschiedene Attribute sowie die dazugehörigen get/set-Methoden:

private string userName = string.Empty;
public string UserName { get { return userName; }set { userName = value; } }

Wenn ich nun etwas in die Textbox eingebe, wird über die Datenbindung der Wert in die Variable zurückgeschrieben und gespeichert. Wenn ich das Form schließe und wieder öffne wird der neue Wert der Variable auch in der Textbox angezeigt. Soweit so gut.

Nun möchte ich aber, dass er diese Werte nur übernimmt, wenn ich den Button "Änderungen übernehmen" drücke und nicht direkt bei der Eingabe. Würde man etwas ändern und z.B. das X-Symbol oder den Button "Abbrechen" drücken, so dürfte nichts übernommen werden und der alte Wert sollte wieder darin stehen. Doch das tut es leider 🙁

Ich habe auch schon probiert, DataSourceUpdateMode.OnValidation zu verwenden, leider ohne Erfolg. Hier werden auch alle Änderungen direkt übernommen, wenn das Textfeld verlassen wird.
Würde ich die Option CausesValidation=false für jedes Textfeld setzen, dann würden zwar die Textfelder nur validieren werden, wenn ich eine entsprechende DoValidate-Methode an den Textfeldern aufrufe, wenn der Button gedrückt wird, dennoch werden auch in diesem Fall nicht alle Werte korrekt gespeichert.

Ich habe dazu ein Bild angehängt, um den Sachverhalt etwas zu verdeutlichen.

Meine Frage besteht nun darin, wie ich die DataBinding-Methode aufrufen muss, damit die Werte zwar gespeichert werden, aber nur dann, wenn auch der Button "Änderungen übernehmen" gedrückt wird?

D
51 Beiträge seit 2014
vor 8 Jahren

Hallo brocel1,

ich bin nicht sicher, ob du das rein durch die Datenbindung so realisieren kannst. So tief bin ich im WPF auch nicht drin, aber:

Wenn du das MVVM-Pattern in deiner Anwendung umsetzt, dann kannst du doch den Button selbst an ein Event bzw. ein Command binden. Dann könntest du den neuen Wert innerhalb dieses Commands "wirksam machen".

Vielleicht kennt jemand aber auch einen direkteren Weg zu deinem Ziel...

W
955 Beiträge seit 2010
vor 8 Jahren

Vielleicht kennt jemand aber auch einen direkteren Weg zu deinem Ziel... Nee, ist richtig so. Erst an einem ViewModel zwischenspeichern und bei "Änderungen übernehmen" ins Model o.ä. kopieren.

K
89 Beiträge seit 2013
vor 8 Jahren

Du hast ja DataSourceUpdateMode.OnPropertyChanged gesetzt. Das heißt, sobald deine Eigenschaft geändert wird wrid diese Übernommen.
Es existiert auch noch OnValidation. Müsstest dann beim betätigen des Buttons validieren und gegenenfalls true zurück geben. Dann werden die Änderungen übernommen.

H
523 Beiträge seit 2008
vor 8 Jahren

Du könntest Dir auch den Zustand der Daten beim Öffnen des Eingabefensters merken und dann beim Abbrechen/Schließen der Maske über das X den gemerkten Zustand wiederherstellen.

Hab's gerade nicht mehr ganz genau im Kopf, aber das lässt sich soweit ich weiß auch mit dem Memento-Pattern abbilden.

F
10.010 Beiträge seit 2004
vor 8 Jahren

@DonStivino:
@witte:
Dies ist Windows Forms, nicht WPF.

@brocel1:
Du solltest die bei Java üblichen Begriffe nicht nach .NET übernehmen.
Das sind Properties und keine Attribute. Attribute haben in Dotnet ein komplett andere Bedeutung.

Ansonsten ist das genau das was Databinding macht, es ist nicht dafür gedacht Businesslogik abzubilden.
Aber dafür gibt es natürlich Pattern und Interfaces in Dotnet.

Als erstes bei DataBinding IMMER INotifyPropertyChanged implementieren.
Brauchst du Validierung wäre IDataErrorInfo wichtig und willst du abbrechbare Edits, implementiere IEdiatbleObject.
Diese 3 Interfaces werden unter Windows Forms von fast allem unterstützt.

B
brocel1 Themenstarter:in
9 Beiträge seit 2015
vor 8 Jahren

Wenn ich die Eigenschaft auf OnValidation setzte, updatet er meine DataSource exakt dann, wenn ich das Textfeld verlasse.

  userTextEdit.DataBindings.Add(new Binding("EditValue",bindingSource,"UserName",true,DataSourceUpdateMode.OnValidation));

Um dies zu verhindern, kann ich für das Textfeld folgende Option festlegen:

this.userTextEdit.CausesValidation = false;

Wenn ich nun den Button "Änderungen übernehmen" drücke, wird das Click-Event aufgerufen.

private void simpleButton2_Click(object sender, EventArgs e)
        {     
            userTextEdit.DoValidate();
            //hier weitere Textfelder updaten
        }

Problem nur ist, dass er nur den Wert eines Textfelder übernimmt und nicht alle. Das gleiche passiert aber auch, wenn ich auf X drücke. Lösche ich alles raus und drücke den X-Button, so steht in einem der Wert - im anderen nicht mehr.

@FZelle:
Das Attribute bei C# eine andere Bedeutung haben, war mir bislang nicht klar. Danke für den Hinweiß.

Ich versuche es mal mit INotifyPropertyChanged Interface umzusetzten.

F
10.010 Beiträge seit 2004
vor 8 Jahren

Nur das du eher IEditableObject benötigst ( also BeginEdit und speziel CancelEdit )

M
198 Beiträge seit 2010
vor 8 Jahren

Hallo!

ich verwende immer


  _curSelectedRow.RejectChanges();

curSelectedRow ist eine DataRow aus einer DataTable vom DataSet

MfG
Mike

B
brocel1 Themenstarter:in
9 Beiträge seit 2015
vor 8 Jahren

Ich habe es hinbekommen:

Zuerst erstelle ich mir eine bindingSource - entweder über den WinForm-Designer - und manuell.
Diese verwaltet alle Bindungen für jedes Windows-Steuerelement:


BindingSource bindingSource = new BindingSource();

Beim Öffnen des Forms, dass die Textfeldern beinhaltet (MyForm), wird der bindingSource die Datenquelle hinzugefügt. MyObject ist eine Klasse, in der Variablen sowie deren dazugehörigen set-/get-Methode deklariert/implementiert sind.

public class MyObject
    {        
        private string userName = string.Empty;
        private string departmentCode = string.Empty;
        //...
        public string UserName { get { return userName; } set { userName = value; } }
        public string DepartmentCode { get { return departmentCode; } set { departmentCode = value; } }

        //...
}
public MyForm()
        {
            InitializeComponent();
            bindingSource.DataSource = MyObject;
       ...
}

Da wir in oben gezeigten Fall zwei Textfelder haben müssen wir diese auch mit unserer bindingSource verknüpfen. Dies geschieht durch:


userTextEdit.DataBindings.Add(new Binding("Text", bindingSource, "UserName"));
departmentTextEdit.DataBindings.Add(new Binding("Text", bindingSource, "DepartmentCode"));

Binding(...) setzt für DataSourceUpdateMode den Defaultwert auf "OnValidation". Das heißt, immer wenn das Textfeld validiert wurde, wird die bindingSource informiert. Wenn wir aber unser Form durch den Button "Abbrechen" oder über das X schließen, soll dies nicht geschehen. Daher müssen wir für jedes Textfeld den DataSourceUpdateMode auf "Never" setzten.

Dazu habe ich mir eine Hilfsklasse erstellt:


 public static class DataBindingUtils
    {
        public static void SuspendTwoWayBinding(BindingManagerBase bindingManager)
        {
            if (bindingManager == null)
            {
                throw new ArgumentNullException("bindingManager");
            }

            foreach (Binding b in bindingManager.Bindings)
            {
                b.DataSourceUpdateMode = DataSourceUpdateMode.Never;
            }
        }

        public static void UpdateDataBoundObject(BindingManagerBase bindingManager)
        {
            if (bindingManager == null)
            {
                throw new ArgumentNullException("bindingManager");
            }

            foreach (Binding b in bindingManager.Bindings)
            {
                b.WriteValue();
            }
        }
    }

Die Klasse DataBindingUtils besitzt zwei Funktionen. Die Funktion "SuspendTwoWayBindung" sorgt dafür, dass der DataSourceUpdateMode aller Bindungen der bindingSource auf "Never gesetzt wird". Die Funktion "UpdateDataBoundObject" sorgt dafür, dass die Werte in die bindingSource übernommen werden.

Die Funktion muss nach dem Festlegen der Bindungen aufgerufen werden!

Also:


public partial class MyForm: Form
    {               
        private BindingSource bindingSource = new BindingSource();
        public MyForm()
        {
            InitializeComponent();
            bindingSource.DataSource = MyObject;

            userTextEdit.DataBindings.Add(new Binding("Text", bindingSource, "UserName"));
            departmentTextEdit.DataBindings.Add(new Binding("Text", bindingSource, "DepartmentCode"));

            DataBindingUtils.SuspendTwoWayBinding(this.BindingContext[bindingSource]);
        }     

Wird nun etwas in die Textfelder eingegeben, so wird die BindingSource nicht informiert. Wird X oder Abbrechen gedrückt, so werden die Änderungen nicht übernommen. Schon mal ein kleines Erfolgserlebnis.

Damit die Änderungen übernommen werden muss beim Drücken des Buttons "Änderungen übernehmen" die andere Funktion der Hilfsklasse "UpdateDataBoundObject(..)" aufgerufen werden.


private void okButton_Click(object sender, EventArgs e)
        {
            DataBindingUtils.UpdateDataBoundObject(this.BindingContext[bindingSource]);
        }

Diese fordert alle Bindungen auf, den aktuellen Wert des Steuerelements (in unserem Fall der Text der Textfelder) der bindingSource zu übergeben. Somit wird nur dann, wenn "Änderungen übernehmen" gedrückt wird die bindingSource informiert.

Und genau das wollte ich. Daher bedanke ich für eure Vorschläge. Einen Weg mit dem IEditObject (Begin,Cancel,End -Edit) würde mich auch interessieren. Ich selber bin damit nicht weiter gekommen..

B
brocel1 Themenstarter:in
9 Beiträge seit 2015
vor 8 Jahren

Scheinbar hab ich etwas gravierendes vergessen. Und zwar muss MyObject das Interface "BusinessObject" implementieren. Dieses wiederrum implementiert die Interfaces "INotifyPropertyChanged, IDataErrorInfo, IEditableObject".

Ohne das Interface "Businessobject" funktioniert es nämlich nicht. Die Klasse habe ich aus einem anderen Projekt entnommen, welches mein Vorgänger erstellt hatte. Diese benötigt noch die Klasse "Rule". Im folgenden könnt ihr beide Klassen begutachten. Vielleicht kann hier jemand erklären, wieso diese zusätzlich noch notwendig sind.


[Serializable()]
    public abstract class BusinessObject : INotifyPropertyChanged, IDataErrorInfo, IEditableObject
    {
        [NonSerialized()]
        private List<Rule> rules;
        [NonSerialized()]
        private MemoryStream memoryStream;
        [NonSerialized()]
        private BinaryFormatter formatter = new BinaryFormatter(null, new StreamingContext(StreamingContextStates.Clone));

        private bool hasBrokenRulesThatMustBeFollowed = false;
        private bool editing = false;

        /// <summary>
        /// Constructor.
        /// </summary>
        public BusinessObject()
        {
        }

        public bool HasBrokenRulesThatMustBeFollowed
        {
            get { return hasBrokenRulesThatMustBeFollowed; }
            protected set { hasBrokenRulesThatMustBeFollowed = value; }
        }

        /// <summary>
        /// Gets a value indicating whether or not this domain object is valid. 
        /// </summary>
        public virtual bool IsValid
        {
            get { return this.Error == null; }
        }        

        /// <summary>
        /// Gets an error message indicating what is wrong with this domain object. The default is an empty string ("").
        /// </summary>
        public virtual string Error
        {
            get
            {
                string result = this[string.Empty];
                if (result != null && result.Trim().Length == 0)
                    result = null;

                return result;
            }
        }

        /// <summary>
        /// Gets the error message for the property with the given Name.
        /// </summary>
        /// <param Name="propertyName">The Name of the property whose error message to get.</param>
        /// <returns>The error message for the property. The default is an empty string ("").</returns>
        public virtual string this[string propertyName]
        {
            get
            {
                var builder = new StringBuilder();

                 propertyName = CleanString(propertyName);

                foreach (Rule r in GetBrokenRules(propertyName))
                    if (propertyName == string.Empty || r.PropertyName == propertyName)
                    {
                        builder.AppendLine(r.Description);
                    }

                return builder.Length != 0 ? builder.ToString() : null;
            }
        }

        /// <summary>
        /// Validates all rules on this domain object, returning a list of the broken rules.
        /// </summary>
        /// <returns>A read-only collection of rules that have been broken.</returns>
        public virtual ReadOnlyCollection<Rule> GetBrokenRules()
        {
            return GetBrokenRules(string.Empty);
        }

        /// <summary>
        /// Validates all rules on this domain object for a given property, returning a list of the broken rules.
        /// </summary>
        /// <param Name="property">The Name of the property to check for. If null or empty, all rules will be checked.</param>
        /// <returns>A read-only collection of rules that have been broken.</returns>
        public virtual ReadOnlyCollection<Rule> GetBrokenRules(string property)
        {
            hasBrokenRulesThatMustBeFollowed = false;

            property = CleanString(property);

            // If we haven't yet created the rules, create them now.
            if (rules == null)
            {
                rules = new List<Rule>();
                rules.AddRange(this.CreateRules());
            }
            List<Rule> broken = new List<Rule>();

            foreach (Rule r in this.rules)
                // Ensure we only validate a rule 
                if (r.PropertyName == property || property == string.Empty)
                {
                    bool isRuleBroken = !r.ValidateRule(this);
                    //Debug.WriteLine(DateTime.Now.ToLongTimeString() + ": Validating the rule: '" + r.ToString() + "' on object '" + this.ToString() + "'. Result = " + ((isRuleBroken == false) ? "Valid" : "Broken"));
                    if (isRuleBroken)
                    {
                        broken.Add(r);
                        if (r.RuleMustBeFollowed)
                            hasBrokenRulesThatMustBeFollowed = true;
                    }
                }

            return broken.AsReadOnly();
        }

        /// <summary>
        /// Occurs when any properties are changed on this object.
        /// </summary>
        [field:NonSerialized]
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Override this method to create your own rules to validate this business object. These rules must all be met before 
        /// the business object is considered valid enough to save to the data store.
        /// </summary>
        /// <returns>A collection of rules to add for this business object.</returns>
        protected virtual List<Rule> CreateRules()
        {
            return new List<Rule>();
        }

        /// <summary>
        /// A helper method that raises the PropertyChanged event for a property.
        /// </summary>
        /// <param Name="propertyNames">The names of the properties that changed.</param>
        protected virtual void NotifyChanged(params string[] propertyNames)
        {
            foreach (string name in propertyNames)
                OnPropertyChanged(new PropertyChangedEventArgs(name));

            OnPropertyChanged(new PropertyChangedEventArgs("IsValid"));
        }

        /// <summary>
        /// Cleans a string by ensuring it isn't null and trimming it.
        /// </summary>
        /// <param name="s">The string to clean.</param>
        protected string CleanString(string s)
        {
            return (s ?? string.Empty).Trim();
        }

        /// <summary>
        /// Raises the PropertyChanged event.
        /// </summary>
        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            if (this.PropertyChanged != null)
                this.PropertyChanged(this, e);
        }

        protected void createSnapShot(object item)
        {
            if (!editing)
            {
                memoryStream = new MemoryStream();
                if (formatter == null)
                    formatter = new BinaryFormatter(null, new StreamingContext(StreamingContextStates.Clone));
                formatter.Serialize(memoryStream, item);

                editing = true;
            }
        }

        protected void restoreSnapshot<T>(ref T item)
        {
            if (editing)
            {
                memoryStream.Position = 0;
                item = (T)formatter.Deserialize(memoryStream);

                editing = true;
            }
        }

        protected void throwAwaySnapshot()
        {
            if (memoryStream != null)
            {
                memoryStream.Dispose();
                memoryStream = null;
            }

            editing = false;
        }

        #region IEditableObject Member

        public virtual void BeginEdit()
        {
            // Childklassen sind für die Implementierung zuständig
        }

        public virtual void CancelEdit()
        {
            // Childklassen sind für die Implementierung zuständig
        }

        public virtual void EndEdit()
        {
            throwAwaySnapshot();         
        }

        #endregion
    }

Klasse Rule:


/// <summary>
    /// An abstract class that contains information about a rule as well as a method to validate it.
    /// </summary>
    /// <remarks>
    /// This class is primarily designed to be used on a domain object to validate a business rule. In most cases, you will want to use the 
    /// concrete class SimpleRule, which just needs you to supply a delegate used for validation. For custom, complex business rules, you can 
    /// extend this class and provide your own method to validate the rule.
    /// </remarks>
    [Serializable]
    public abstract class Rule
    {
        private string _description;
        private string _propertyName;
        private bool _ruleMustBeFollowed = false;

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="propertyName">The Name of the property the rule is based on. This may be blank if the rule is not for any specific property.</param>
        /// <param name="brokenDescription">A description of the rule that will be shown if the rule is broken.</param>
        public Rule(string propertyName, string brokenDescription)
        {
            this.Description = brokenDescription;
            this.PropertyName = propertyName;
        }

        public Rule(string propertyName, string brokenDescription, bool ruleMustBeFollowed)
        {
            this.Description = brokenDescription;
            this.PropertyName = propertyName;
            this._ruleMustBeFollowed = ruleMustBeFollowed;
        }

        public bool RuleMustBeFollowed
        {
            get { return _ruleMustBeFollowed; }
        }

        /// <summary>
        /// Gets descriptive text about this broken rule.
        /// </summary>
        public virtual string Description
        {
            get { return _description; }
            protected set { _description = value; }
        }

        /// <summary>
        /// Gets the Name of the property the rule belongs to.
        /// </summary>
        public virtual string PropertyName
        {
            get { return (_propertyName ?? string.Empty).Trim(); }
            protected set { _propertyName = value; }
        }

        /// <summary>
        /// Validates that the rule has been followed.
        /// </summary>
        public abstract bool ValidateRule(BusinessObject domainObject);

        /// <summary>
        /// Gets a string representation of this rule.
        /// </summary>
        /// <returns>A string containing the description of the rule.</returns>
        public override string ToString()
        {
            return this.Description;
        }

        /// <summary>
        /// Serves as a hash function for a particular type. System.Object.GetHashCode()
        /// is suitable for use in hashing algorithms and data structures like a hash
        /// table.
        /// </summary>
        /// <returns>A hash code for the current rule.</returns>
        public override int GetHashCode()
        {
            return this.ToString().GetHashCode();
        }
    }

Somit die Klasse MyObject:


public class MyObject: BusinessObject
    {
//...
    }

F
10.010 Beiträge seit 2004
vor 8 Jahren

Man kann sich das Leben auch echt schwer machen.

Die beiden Klassen sind nicht notwendig, der Entwickler von dem du die Vorlage hast, hat es halt so implementiert.
Dadurch kannst du BusinessRules implementieren wie z.b. Required, irgendwelche string längen usw ( also IDataErrorInfo ).

Auch IEditableObject ist nur vorbereitet.
Im Grunde genommen brauchst du nur etwas wie Referenztyp: im Bearbeitenfenster soll nur bei OK die Änderung übernommen werden