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.
Related
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.
I'm fairly new to C# but understand basic concepts.
I'm currently working on a Uni assignment where I have to have multiple textboxes be entered as a single entry in a listbox, then save all entries to a text file. I also need to be able to load the text file and add new entries to the list.
I've figured out how to save data to a .txt file, as well as reloading the .txt file back into the listbox using
if (File.Exists("PersonalFile.txt"))
{
string[] line = File.ReadAllLines("PersonalFile.txt");
lbxStaffDetails.ItemsSource = line;
}
However, doing it this way I can't add new entries to the listbox due to the data binding, I get this error message System.InvalidOperationException: 'Operation is not valid while ItemsSource is in use. Access and modify elements with ItemsControl.ItemsSource instead.'
Is there a way to remove the binding but keep the data in the listbox? Using lbxStaffDetails.ItemsSource = null; clears the listbox; or is there another way to read all lines of the .txt file to the listbox without using the file as a binding source?
Notes:
lbxStaffDetails is this listbox in question
PersonalFile.txt is the .txt holding the entries on new lines.
This is the first time I've bound data and files.
Edit:
Forgot to mention how I'm adding the data to the listbox so here's the code for that.
private void btnAddWaitingList_Click(object sender, RoutedEventArgs e)
{
_EmployeeID = tbxEmployeeID.Text;
_Name = tbxName.Text;
_PayRate = tbxPayRate.Text;
_Email = tbxEmail.Text;
string employeeDetails = _EmployeeID + "," + _Name + "," + _PayRate + "," + _Email;
lbxStaffDetails.Items.Add(employeeDetails);
}
As the code fires and gets to the bottom line it throws the error mentioned above.
Don't confuse data binding with simple value assignment. Data binding is a different concept, where a target binds to a data source using a Binding. A Binding will monitor target and source and delegates changes from one to the other. It's a bi-directional dynamic data link.
You can setup a Binding in XAML or C# (see Data binding overview in WPF).
You are not binding the ListBox to a file. You have read contents of a file to an array of strings. This array is then assigned to the ListBox.ItemsSource property.
Since you have populated the ListBox using the ItemsSource property, you are not allowed to modify its items using the Items property (InvalidOperationException).
You have to either assign the modified collection again to ListBox.ItemsSource (which will cause the complete ListBox to create all items again, which is bad for the performance) or make use of ObservableCollection.
It's a special collection that allows to be observed (Observer pattern). The observer gets notified by the observed collection via an event that the collection has changed (add/move/remove). Every ItemsControl is able to listen to this event and will automatically update itself.
MainWindow.xaml.cs
partial class MainWindow : Window
{
public ObservableCollection<string> StaffDetails { get; set; }
public MainWindow()
{
InitializeComponent();
// Set the DataContext to MainWindow for data binding (XAML version)
this.DataContext = this;
}
private void ReadFile()
{
if (!File.Exists("PersonalFile.txt"))
{
return;
}
string[] lines = File.ReadAllLines("PersonalFile.txt");
// Create a new ObservableCollection and initialize it with the array
this.StaffDetails = new ObservableCollection<string>(lines);
// You write to the file using this same collection directly,
// without accessing the ListBox
File.WriteAllLines("PersonalFile.txt", this.StaffDetails);
// Option 1: assignment (static data link)
this.lbxStaffDetails.ItemsSource = this.StaffDetails;
// Alternative option 2: C# data binding (dynamic data link)
var binding = new Binding(nameof(this.StaffDetails)) { Source = this };
this.lbxStaffDetails.SetBinding(ItemsControl.ItemsSourceProperty, binding);
// Alternative option 3 (recommended): XAML data binding (dynamic data link). See MainWindow.xaml
}
private void btnAddWaitingList_Click(object sender, RoutedEventArgs e)
{
_EmployeeID = tbxEmployeeID.Text;
_Name = tbxName.Text;
_PayRate = tbxPayRate.Text;
_Email = tbxEmail.Text;
var employeeDetails = $"{_EmployeeID},{_Name},{_PayRate},{_Email}";
// Modify the ObservableCollection.
// Since ListBox is observing this collection, it will automatically update itself
this.StaffDetails.Add(employeeDetails);
}
}
MainWindow.xaml
<Window>
<!-- Alternative option 3: XAML data binding (recommended) -->
<ListBox x:Name="lbxStaffDetails"
ItemsSource="{Binding StaffDetails}" />
</Window>
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?
I'm using C# WPF MVVM. So in XAML there is a listview, which is binded to an object, used to show different information from sql database depending on tab.
For example. I have two forms: one is that shows information and another that is used to input information. How can I automatically update the listview in one form, after new information was entered in another form? Because now I have to switch tabs to get the listview updated.
binding direction for this element should be exposed to TwoWay (Mode=TwoWay)
like this:
x:Name="list"
ItemsSource="{Binding ....... , Path=........., Mode=TwoWay}}" ......
Apart from the default binding, which is one way, you can also configure binding to be two way, one way to source, and so forth. This is done by specifying the Mode property.
OneWay: Causes changes to the source property to automatically update the target property but the source does not get changed
TwoWay: Changes in the source or target automatically cause updates to the other
OneWayToSource: Causes changes to the target property to automatically update the source property but the target does not get changed
OneTime: Causes only the first time change to the source property to automatically update the target property but the source does not get changed and subsequent changes do not affect the target property
you can look this : http://msdn.microsoft.com/en-us/library/ms752347.aspx
After you enter new information into a form, try to invoke your own method, which will update your information into a list view.
So you can use some event eg. DataContentChanged or your update method can be called when u click the button which adds new data into your form.
Example of refresh method should look like this:
public void lbRefresh()
{
//create itemsList for listbox
ArrayList itemsList = new ArrayList();
//count how many information you wana to add
//here I count how many columns I have in dataGrid1
int count = dataGrid1.Columns.Count;
//for cycle to add my strings of columns headers into an itemsList
for (int i = 0; i < count; i++)
{
itemsList.Add(dataGrid1.Columns[i].Header.ToString());
}
//simply refresh my itemsList into my listBox1
listBox1.ItemsSource = itemsList;
}
EDIT: To finish and solve your problem, try to use this snippet of code:
//some btn_Click Event in one window
//(lets say, its your callback " to update" button in datagrid)
private void Button_Click_1(object sender, RoutedEventArgs e)
{
//here you doing somethin
//after your datagrid got updated, try to store the object,
//which u want to send into your eg. listbox
data[0] = data; //my stored data in array
//for better understanding, this method "Button_Click_1" is called from Window1.xaml.cs
//and I want to pass information into my another window Graph1.xaml.cs
//create "newWindow" object onto your another window and send "data" by constuctor
var newWindow = new Graph1(data); //line *
//you can call this if u want to show that window after changes applied
newWindow.Show();
}
After that your Graph1.xaml.cs should look like this:
public partial class Graph1 : Window
{//this method takes over your data u sent by line * into previous method explained
public Graph1(int[]data)
{
InitializeComponent();
//now you can direcly use your "data" or can call another method and pass your data into it
ownListBoxUpdateMethod(data);
}
private void ownListBoxUpdateMethod(int[] data)
{
//update your listbox here and its done ;-)
}
I've created a control derived from ComboBox, and wish to unit test its behaviour.
However, it appears to be behaving differently in my unit test to how it behaves in the real application.
In the real application, the Combobox.DataSource property and the .Items sync up - in other words when I change the Combobox.DataSource the .Items list immediately and automatically updates to show an item for each element of the DataSource.
In my test, I construct a ComboBox, assign a datasource to it, but the .Items list doesn't get updated at all, remaining at 0 items. Thus, when I try to update the .SelectedIndex to 0 in the test to select the first item, I recieve an ArgumentOutOfRangeException.
Is this because I don't have an Application.Run in my unit test starting an event loop, or is this a bit of a red herring?
EDIT: More detail on the first test:
[SetUp]
public void SetUp()
{
mECB = new EnhancedComboBox();
mECB.FormattingEnabled = true;
mECB.Location = new System.Drawing.Point( 45, 4 );
mECB.Name = "cboFind";
mECB.Size = new System.Drawing.Size( 121, 21 );
mECB.TabIndex = 3;
mECB.AddObserver( this );
mTestItems = new List<TestItem>();
mTestItems.Add( new TestItem() { Value = "Billy" } );
mTestItems.Add( new TestItem() { Value = "Bob" } );
mTestItems.Add( new TestItem() { Value = "Blues" } );
mECB.DataSource = mTestItems;
mECB.Reset();
mObservedValue = null;
}
[Test]
public void Test01_UpdateObserver()
{
mECB.SelectedIndex = 0;
Assert.AreEqual( "Billy", mObservedValue.Value );
}
The test fails on the first line, when trying to set the SelectedIndex to 0. On debugging, this appears to be because when the .DataSource is changed, the .Items collection is not updated to reflect this. However, on debugging the real application, the .Items collection is always updated when the .DataSource changes.
Surely I don't have to actually render the ComboBox in the test, I don't even have any drawing surfaces set up to render on to! Maybe the only answer I need is "How do I make the ComboBox update in the same way as when it is drawn, in a unit test scenario where I don't actually need to draw the box?"
Since you're simply calling the constructor, a lot of functionality of the combobox will not work. For example, the items will be filled when the ComboBox is drawn on screen, on a form. This does not happen when constructing it in a unit test.
Why do you want to write a unit test on that combobox?
Can't you seperate the logic which now is in the custom control? For example put this in a controller, and test that?
Why don't you test on the DataSource property instead of the Items collection?
I'm sure that Application.Run absence cannot affects any control's behavior
I'm having the same problem with a combo box where the items are data bound. My current solution is to create a Form in the test, add the combo box to the Controls collection, and then show the form in my test. Kind of ugly. All my combo box really does is list a bunch of TimeSpan objects, sorted, and with custom formatting of the TimeSpan values. It also has special behavior on keypress events. I tried extracting all the data and logic to a separate class but couldn't figure it out. There probably is a better solution but what I'm doing seems satisfactory.
To make testing easier, I created these classes in my test code:
class TestCombo : DurationComboBox {
public void SimulateKeyUp(Keys keys) { base.OnKeyUp(new KeyEventArgs(keys)); }
public DataView DataView { get { return DataSource as DataView; } }
public IEnumerable<DataRowView> Rows() { return (DataView as IEnumerable).Cast<DataRowView>(); }
public IEnumerable<int> Minutes() { return Rows().Select(row => (int)row["Minutes"]); }
}
class Target {
public TestCombo Combo { get; private set; }
public Form Form { get; private set; }
public Target() {
Combo = new TestCombo();
Form = new Form();
Form.Controls.Add(Combo);
Form.Show();
}
}
Here is a sample test:
[TestMethod()]
public void ConstructorCreatesEmptyList() {
Target t = new Target();
Assert.AreEqual<int>(0, t.Combo.DataView.Count);
Assert.AreEqual<int>(-1, t.Combo.SelectedMinutes);
Assert.IsNull(t.Combo.SelectedItem);
}
This solve some problems if target is ComboBox or any other control:
target.CreateControl();
but I was unable to set SelectedValue it has null value, my test working with two data sources for combo box, one as data source and second is binded to selevted value. With other controls everithing working fine. In the begining I was also creating form in tests, but there is problem when form on created on our build server while tests are executed.
I did a little hack to allow this in my custom derived combobox:
public class EnhancedComboBox : ComboBox
{
[... the implementation]
public void DoRefreshItems()
{
SetItemsCore(DataSource as IList);
}
}
The SetItemsCore function instructs the base combobox to load internal items with the provided list, it's what uses internally after the datasource changes.
This function never gets called when the control is not on a form, because there are lots of checks for CurrencyManagers and BindingContexts that are failing because this components, I believe, are provided by the parent form somehow.
Anyway, in the test, you have to call mECB.DoRefreshItems() just after the mECB.DataSource = mTestItems and everything should be fine if you only depend on the SelectedIndex and the Items property. Any other behavior like databinding is probably still not functional.