I am working on some data collection forms in WinForms/C#. When the form loads, I am looping through a configuration and adding a new Binding to each of the TextBox controls; mapping the Text property of each TextBox control to specific string property on my POCO object.
public void BindTextBoxControls(dynamic entity, List<TextBoxConfig> textBoxConfig)
{
foreach (var config in textBoxConfig)
config.Control.DataBindings.Add(new Binding("Text", entity, config.PropertyName));
}
Everything has been working as expected, new records properly saving new values entered into the corresponding TextBox controls, TextBoxes populating with the correct values when reopened a previously entered records with the form, and updates to values in TextBoxes of previously entered records are getting the updated values set on the underlying POCO.
However, I started to layer in some business rules onto the form specifically to gray out/disable and clear out previously entered values in the TextBox based on other user input/activity on the form - things are not working as expected.
In a contrived example; a rule like if a Checkbox_1 is checked then TextBox #5 should not be valued (clear out any previously entered value and disable it from input). On my Checkbox_1 event handler for CheckedChanged, I specifically check if the Checkbox_1 is checked and if so, set TextBox_1.Text == null and TextBox_1.Enabled = false. This works as expected and on the form, I see any previously entered value cleared from the TextBox_1 and it becomes enabled.
private void chkCheckBox1_CheckedChanged(object sender, EventArgs e)
{
if(!chkCheckBox1.Checked)
{
txtBox5.Text = string.Empty;
}
}
However, when I debug and break on the save and inspect the underlying POCO's property that the underlying control is bound to after the method is called; the old value still persists on the object's property which the text box is bound to, despite the textbox having not value appearing on the form. When I reopen the form for that record, the old cleared out value is re-populated in the disabled TextBox. However, manually clearing out the value in the same TextBox or updating a value and inspecting the object shows the updated value after those operations are performed.
It seems like changing the Text value of a TextBox control (e.g. the Text property of a TextBox) in code maybe somehow be "bypassing" the DataBinding? I'm actually seeing the same/similar behavior when applying similar rules to "uncheck" TextBoxes programmatically within event handler methods - the CheckBox controls are also using DataBinding to boolean properties on the POCO.
When you setup databinding by this overload: Binding(String, Object, String), then the value of DataSourceUpdateMode will be OnValidation, which means when you modify the value of control's property using code or through UI, the binding will push the new value to data source only after Validating event happens for the control.
To fix the problem, use either of the following options:
Use another overload and set the DataSourceUpdateMode to OnProperetyChanged
OR, after setting the Value of the TextBox.Text call ValidateChildren method of the form.
Example - Set the DataSourceUpdateMode to OnProperetyChanged
public class Person
{
public string Name { get; set; }
public string LegalCode { get; set; }
public bool IsRealPerson { get; set; }
}
Person person;
private void Form1_Load(object sender, EventArgs e)
{
person = new Person() {
Name = "My Company", LegalCode = "1234567890", IsRealPerson = false };
NameTextBox.DataBindings.Add(nameof(TextBox.Text), person,
nameof(Person.Name), true, DataSourceUpdateMode.OnPropertyChanged);
LegalCodeTextBox.DataBindings.Add(nameof(TextBox.Text), person,
nameof(Person.LegalCode), true, DataSourceUpdateMode.OnPropertyChanged);
IsRealPersonCheckBox.DataBindings.Add(nameof(CheckBox.Checked), person,
nameof(Person.IsRealPerson), true, DataSourceUpdateMode.OnPropertyChanged);
IsRealPersonCheckBox.CheckedChanged += (obj, args) =>
{
if (IsRealPersonCheckBox.Checked)
{
LegalCodeTextBox.Text = null;
LegalCodeTextBox.Enabled = false;
}
};
}
Note - You can put the logic inside the model
Another solution (Which needs more effort and more changes in your code) is implementing INotifyPropertyChanged in your model class. Then when PropertyChanged event raises for your boolean property, you can check if it's false then you can set the string property to null.
In this approach you don't need to handle UI events. Also right after updating the model property, the UI will be updated; in fact implementing INotifyPropertyChanged enables two-way databinding for your model class.
Related
I have a windows form with a ComboBox DisplayBox. In my ViewModel I now have a Property BindingList<MyObject> ObjectBindingList that I want to bind to the DisplayBox.
When I load the form, the DisplayBox does not show any text.
The property DataSource is set and holds a List of MyObjects when checking in the debug modus after the data download.
The property items always has a count of zero.
My code works as following:
On startup I set the databindings in the form class to a still empty List ObjectBindingList.
displayBox.DataSource = ObjectBindingList;
The DisplayMember and ValueMember were set in the ComboBox Properties in the GUI Designer.
Asynchrously the controller downloads some data (MyDataObjects) async. Then sets the BindingList<MyObject> ObjectBindingList in the ViewModel to the downloaded Objects through adding them.
Since I don't see all of the relevant code, I can only assume what's happening.
Probably, you don't see the data in the ComboBox, because you are creating a new BindingList when loading the data. But the ComboBox is still attached to the old empty list.
You initialize the data source with an empty list like this:
// Property
BindingList<MyObject> ObjectBindingList { get; set; }
Somewhere else
// Initializes data source with an empty `BindingList<MyObject>`.
ObjectBindingList = new BindingList<MyObject>();
displayBox.DataSource = ObjectBindingList;
Later, you load the data and replace the list:
ObjectBindingList = LoadData();
Now, you have two lists: the initial empty list assigned to displayBox.DataSource and a new filled one assigned to the property ObjectBindingList. Note that displayBox.DataSource does not have a reference to the property itself, therefore it does not see the new value of the property.
For a BindingList<T> to work as intended, you must add the items with
var records = LoadData();
foreach (var data in records) {
ObjectBindingList.Add(data);
}
I.e., keep the original BindingList<MyObject> assigned to the data source.
See also: How can I improve performance of an AddRange method on a custom BindingList?
To avoid the problem, I would be advisasble to make the property read-only (using C# 9.0's Target-typed new expressions).
BindingList<MyObject> ObjectBindingList { get; } = new();
It seems like when trying to update the ComboBox from a different thread than the main forms thread, the update did not reach the control.
I am now using the Invoke Method together with a BindingSource Object in between the Binding List and the control.
private void SetBindingSourceDataSource( BindingList<MyObject> myBindingList)
{
if (InvokeRequired)
{
Invoke(new Action<BindingList<MyObject>>(SetBindingSourceDataSource), myBindingList);
}
else {
this.BindingSource.DataSource = myBindingList;
}
}
I am expeciall calling the above function on a PropertyChanged event, that I trigger at the end of every call of the download Function.
I have an object that I have bound to a control on a form using C# WinForms (targetting .NET 4.5.2). I have implemented INotifyPropertyChanged, and when I modify a Property of this object, it updates on the Form's control as expected. However, when I change this object's instance to a new instance, it will no longer update the control, even if I try to modify a specific Property.
class User : INotifyPropertyChanged
{
string _name = "";
public string Name
{
get { return _name; }
set { _name = value; OnPropertyChanged("Name"); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string property)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
public User() { }
public User(string name)
{
Name = name;
}
public User(User otherUser)
{
Name = otherUser.Name;
}
}
and on the Form I have
User currentUser = new User("Example Name");
lblName.DataBindings.Add("Text", currentUser, "Name");
which updates properly. I can change name using currentUser.Name = "Blahblah"; and it works fine. When I try to call currentUser = new User("New Name");, it will no longer update no matter what I do.
It is my understanding that the specific instance of the object is what the controls are bound to. My primary goal is to not have to manually go through each Property of larger objects and manually change everything over every single time I want to change instances.
My question: is there a way to change instances of an object without removing the binding to the control?
To get the desired behavior, you should not bind directly to the concrete instance like you do currently:
lblName.DataBindings.Add("Text", currentUser, "Name");
Instead, you need some intermediary. The easiest is to use the BindingSource component for that purpose.
Add a BindingSource field to the form:
private BindingSource dataSource;
Then initialize it and bind the controls to it one time (usually in form Load event):
dataSource = new BindingSource { DataSource = typeof(User) };
lblName.DataBindings.Add("Text", dataSource, "Name");
// ...
Now anytime you want to bind to a User instance, you simply assign it to the DataSource property of the BindingSource:
initial:
dataSource.DataSource = new User("Example Name");
later:
dataSource.DataSource = new User("New Name");
Ivan showed you how to solve the problem. Here in this answer I'll try to show you what the problem is and why it works this way.
Short answer
Your label is bound to an object not the variable. The variable is just like a pointer to the object. So replacing the variable doesn't have any impact on the object which your label is using as data source. Just the variable points to the new object. But your label is using the previous object. It has its own pointer to the previous object.
Consider these facts to know what is happening:
User currentUser = new User("Example Name");
When you assign an object to a variable, in fact the variable points to the object.
lblName.DataBindings.Add("Text", currentUser, "Name");
You performed data-binding to the object. The control will be bound to the object not the variable.
currentUser.Name = "Blahblah";
You changed the value of Name property and since you implemented INotifyPropertyChanged the control will be notified of changes and will refresh its Text.
But the main statement which didn't work as you expected:
currentUser = new User("New Name");
Here you made the currentUser variable point to another object. It doesn't have anything to do with the previous object which it was pointing to. It just points to another object.
The Binding which you added to the label, still uses the previous object.
So it's normal to not have any notification, because the object which is in use by the label didn't changed.
How the BindingSource solved the problem?
The BindingSource raises ListChanged event when you assign a new object to its DataSource and it makes the BindingObject fetch new value from DataSource and refresh Text property. You can take a look at this post: How to make a binding source aware of changes in its data source?
Is is possible to make a binding in WPF whereas the source and target are different properties.
A la something like
Binding="{Binding Source=MySourceProperty, Target=MyTargetProperty}"
As requested an explanation of what I need to do:
The program among other things allows editing of properties that are part of a primary key in the database. If the property just gets changed, then this will either not update the DB value or create a duplicate, depending on how I handle saving the object. A different target would allow this to work (by explicitly specifying what to update by using the 'old' value).
A Binding defined in XAML is always targeting the object and property on which it's defined.
If you define the Binding in code, you can/must specify the source and target explicitly. This is, essentially, how the Binding class works:
Binding binding = new Binding("SourceProperty"); // Sets up the source property
myBinding.Source = mySourceObject; // sets up the source object
targetProperty.SetBinding(TargetType.TargetDepProperty, binding); // This sets the target object/binding
The XAML markup extension for a binding takes care of setting up the target side of the equation automatically, so it's always the object on which you define the binding.
I'll try to answer WHAT you need instead of asked incorrect HOW
"If the property just gets changed, then this will either not update
the DB value or create a duplicate"
In your property setter you should check set { if (this.someMember != value) if the typed in value is has changed:
public event PropertyChangedEventHandler PropertyChanged;
private string someMember;
public int SomeProperty
{
get
{ return this.someMember; }
set
{
if (this.someMember != value)
{
someMember = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("SomeProperty"));
}
}
}
As aside-note (or off-topic),
you might find useful the codeproject DataContext in WPF article in its last Download the source code has a sample when updates of one VisualModel's property is reflected (synchronized with updates in the other VM's property)
Immediately after launch:
The text typed in the 1st textbox is reflected in the 2nd textbox and vice versa, the text typed in the 2nd textbox is reflected in the 1st.
The text typed in the 3d textbox is reflected in the 4th textbox (and in textblock content at bottom) and vice versa, the text typed in the 4th textbox is reflected in the 3d (and in textblock content at bottom) .
Note that the download is DataCotext Inner Objects.zip which is unzipped into directory and solution with name Bindingtoclassesstate
In the set of the public property you have access to the old value
key is the old value
value is the proposed value from the binding
you can reject the value that comes from the binding
private string key;
public string Key
{
get { return key; }
set
{
if (key == value) return;
// try data update
bool success = updateDB();
if (success) key = value; // only update if success
}
}
I would combine the above with validation to notify the user if a value was invalid.
Validation Class
I have an Employee object that has an EmploymentStatusID (int) field.
I have a combobox that is filled from an Employment Status enum and bound to the field in the Form_Load:
List<LookupListItem> EmpStatuses = new List<LookupListItem>();
foreach (EmploymentStatuses m in Enum.GetValues(typeof(EmploymentStatuses)))
{
EmpStatuses.Add(new LookupListItem((int)m, m.ToString()));
}
cboStatus.DataSource = EmpStatuses; // Enum.GetValues(typeof(CommonLibrary.Lookups.EmploymentStatuses));
cboStatus.ValueMember = "ItemValue";
cboStatus.DisplayMember = "ItemDesc";
cboStatus.DataBindings.Add("SelectedValue", _presenter.SelectedOfficer, "EmploymentStatusID");
When the form comes up the correct value is displayed in the combobox, but if the user changes the value, it is set back when the combobox loses focus!
Text boxes and simple comboboxes (ie ones with a string collection) on the same form are fine.
You can see that I originally tried just using GetValues on the enum, but I changed it to a list to see if that would help. I've tried using a BindingList, I've tried using DataSourceUpdateMode.OnValidation on the binding. I even tried using cboStatus.DataBindings[0].WriteValue on the selectedindexchanged event. No matter what I do, the value changes back to what it was when the form opened! Any ideas?
i modified your code
List<LookupListItem> EmpStatuses = new List<LookupListItem>();
foreach (EmploymentStatuses m in Enum.GetValues(typeof(EmploymentStatuses)))
{
EmpStatuses.Add(new LookupListItem((int)m, m.ToString()));
}
EmpStatuses.Add(new LookupListItem(<selectedValue>, "SomeText")); //<- my modified code
cboStatus.DataSource = EmpStatuses; // Enum.GetValues(typeof(CommonLibrary.Lookups.EmploymentStatuses));
cboStatus.ValueMember = "ItemValue";
cboStatus.DisplayMember = "ItemDesc";
// Remove this part cboStatus.DataBindings.Add("SelectedValue", _presenter.SelectedOfficer, "EmploymentStatusID");
cboStatus.SelectedValue = <selectedValue> //<- my modified code
i hope this will help :)
I have a textbox and have an onlostfocus event on it.
Inside the lostfocus method, is there a way I can determine if the user has actually changed the value in it?
i.e how do i get hold of any previous value in it?
Thanks
As with just about everything else in WPF, this is easier if you use data binding.
Bind the text box to a class property. By default, bindings update the source when the bound control loses focus, so you don't have to muck around with the LostFocus event. You then have access to both the new value and the value that the user entered in the property setter.
In the XAML it looks like this:
<TextBox Text="{Binding MyProperty, Mode=TwoWay}"/>
In the class it looks like this:
private string _MyProperty;
public string MyProperty
{
get { return _MyProperty; }
set
{
// at this point, value contains what the user just typed, and
// _MyProperty contains the property's previous value.
if (value != _MyProperty)
{
_MyProperty = value;
// assuming you've implemented INotifyPropertyChanged in the usual way...
OnPropertyChanged("MyProperty");
}
}
What comes to mind for me is a two stage approach. Handle the TextChanged event on the textbox and flag it. Then when the textbox OnLostFocus occurs you can simply check your flag to see if the text has been changed.
Here is a code snippet on how you could handle the tracking.
public class MyView
{
private bool _textChanged = false;
private String _oldValue = String.Empty;
TextChanged( ... )
{
// The user modifed the text, set our flag
_textChanged = true;
}
OnLostFocus( ... )
{
// Has the text changed?
if( _textChanged )
{
// Do work with _oldValue and the
// current value of the textbox
// Finished work save the new value as old
_oldValue = myTextBox.Text;
// Reset changed flag
_textChanged = false;
}
}
}
Store the original value somewhere. You could write a common component to store the value when it gets focus and compare the value when it loses focus. I've done this in ASP.NET and it works quite well.
Another way to solve this by databinding:
Bind the TextBox.Text to the property, that holds the inital value, but use a binding with
UpdateSourceTrigger=Explicit
Then, when the textbox loses focus, you can check the binding if source and target values differ, using this code snippet and evaluating the resulting BindingExpression:
BindingExpression be = tb.GetBindingExpression(TextBox.TextProperty);
Some more code can be found here:
http://bea.stollnitz.com/blog/?p=41