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.
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 two ComboBoxes envelopeList and datasetList. Consider the following line of code:
envelopeList.DataBindings.Add("DataSource", datasetList, "SelectedValue");
The intended functionality is to update envelopeList.DataSource to be datasetList.SelectedValue whenever the selection is changed. However, if datasetList is empty this throws an ArgumentException saying "Additional information: Complex DataBinding accepts as a data source either an IList or an IListSource."
I don't understand why this happens. When datasetList is empty datasetList.SelectedValue returns null and envelopeList.DataSource = null does not throw any exception. This doesn't throw any exceptions either: envelopeList.DataSource = datasetList.SelectedValue;, nor does this: envelopeList.DataSource = new BindingSource(datasetList, "SelectedValue");, even when datasetList is empty.
Doing the binding after datasetList has at least one item works as intended, until it becomes empty in which case envelopeList.DataSource isn't updated. The DataSourceChanged event isn't even fired. (Though in my case that noticed by the user since the DataSource will be emptied when the item in datasetList is deleted).
To make this work I have to execute the following code after datasetList has been populated for the first time:
if(doonce && !(doonce = false))
envelopeList.DataBindings.Add("DataSource", datasetList, "SelectedValue");
It's a very ugly way to do it and I would much rather be able to do this during initialization.
Some potentially important information.
Both ComboBoxes are actually my own inheriting type AdvancedComboBox. This is the relevant functionality within:
public class AdvancedComboBox : ComboBox, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected override void OnSelectedValueChanged(EventArgs e)
{
base.OnSelectedValueChanged(e);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("SelectedValue")); //I don't know why, but it works even if I remove this line.
}
}
(I have other uses for the PropertyChanged event, even if I apparently don't actually need it for the SelectedValue property.)
datasetList.DataSource is bound to an IBindingList containing DatasetPresenter objects.
DatasetPresenter has a property Areas that return an IBindingList with the objects that I want envelopeList to show.
I run datasetList.ValueMember = "Areas" before doing the binding.
The question
How do I make envelopeList.DataBindings.Add("DataSource", datasetList, "SelectedValue"); work even when datasetList is empty or achive a similar result?
I prefer solutions that I only have to execute during initialization of the ComboBoxes and/or code that I can put inside the AdvancedComboBox class so that it remains self-containing.
Bonus: Why doesn't it work when datasetList is empty? Even though datasetList.SelectedValue returns null and envelopeList.DataSource = null is okay.
Well, I found a solution, but it's not pretty.
I created this class specifically for this purpose:
private class PlaceHolder
{
public string Name { get; }//This property is the DisplayMember of both ComboBoxes.
public List<PlaceHolder> Areas { get { return new List<PlaceHolder>(0); } }//This property is the ValueMember of datasetList.
}
And during initialization I run this code.
datasetList.DataSource = new List<PlaceHolder>() { new PlaceHolder() };
envelopeList.DataBindings.Add("DataSource", datasetList, "SelectedValue");
datasetList.DataSource = myController.Datasets;//This is the intended data source.
It's ugly and I'm still looking for better solutions.
This question is a result of the fix to this problem. After getting the sort behavior to properly sort the ObservableCollection, I now notice that the first time the collection is added to, the CustomSorter handler fires with only the first item in it, and that same item is passed in as both parameters to the method. That is producing a duplicate item in the list.
Here are the pertinent parts of the view model code:
public ObservableCollection<PermissionItemViewModel> PermissionItems { get; private set; }
private void FetchRoleData()
{
PermissionItems.Clear();
if (SelectedRole != null)
{
using (var context = new myDataContext(new myDbFactory().GetConnectionString()))
{
foreach (PermissionsEnum permission in Enum.GetValues(typeof(PermissionsEnum)))
PermissionItems.Add(new PermissionItemViewModel(permission, SelectedRole[permission]));
}
}
}
All subsequent manipulations of that collection do not do this...it only happens the first time through the FetchRoleData method. Why?
EDIT:
Some additional information. The CustomSort property is set when the CollectionViewSource fires its Filter event (the only event it has AFAIK). I couldn't come up with any better trigger to get it set. The OnAttached override is too soon, as the View member of the CollectionViewSource is not set yet by that point. Catch 22, I guess. That is happening immediately after that first collection item is added. If this is due to the order in which things are being set, then are there any recommendations for a change?
I don't know how or where you're setting up the filter handler. Here's an example of how to set a custom sort on a CollectionViewSource when its View property changes. That's when you want to do it. This assumes that it's in the resources for a Window (or at least someplace where the Window can find it). But once you have cvs, wherever it comes from and however you got your mitts on it, the rest is the same.
public MainWindow()
{
InitializeComponent();
var cvs = FindResource("MyCollectionViewSource1") as CollectionViewSource;
var dpdView = DependencyPropertyDescriptor.FromProperty(
CollectionViewSource.ViewProperty, typeof(CollectionViewSource));
dpdView.AddValueChanged(cvs, (s, e) =>
{
if (cvs.View is ListCollectionView lvc)
{
lvc.CustomSort = new CustomSorter();
}
});
}
I'm baffled by your claim that the first item in the collection is being duplicated. No code you've shown, and no code I've given you, could have that effect. You'll have to share code that demonstrates that issue.
I'm working on a small program as part of my A Level Computing course that is designed to track orders. It is written in C# using the Windows Forms.
I am having an issue where I enter all the information for a new order and then press OK and it should update the ListView with the information. I have my ListView in Detail view with 4 columns but nothing ever gets added to the ListView. The section of code that should add the items to the ListView is being executed and is not throwing any errors or causing the program to crash but nothing is being added. Its weird because I am using the exact same method that I used in my little prototype mock up but for some reason now it is not working.
All the things I've found on here or on the internet seem to suggest its an issue with the View mode of the ListView and I've tried modifying this property to no avail.
Any ideas why this section of code is refusing to add anything to the ListView?
//Create an array to store the data to be added to the listbox
string[] orderDetails = { Convert.ToString(id + 1), rNameBox.Text, dateBox.Value.ToString(), orderBox.Text };
//DEBUGGING
Console.WriteLine(orderDetails[0]);
Console.WriteLine(orderDetails[1]);
Console.WriteLine(orderDetails[2]);
Console.WriteLine(orderDetails[3]);
//END DEBUGGING
//Add the order info to the ListView item on the main form
var listViewItem = new ListViewItem(orderDetails);
ths.listView1.Items.Add(listViewItem);
If you need any more information just say. Apologies if this is in the wrong format or something this is my first time here.
Your problem is that your ListViewItem contains a string array and it has no useful way of displaying it.
What you should be doing (there are a number of ways of doing this, but here's one) is creating a class, OrderDetail, with an Id, a Name, a Date, and so on. Give it a ToString() method (public override string ToString()) which returns what you want to display, e.g.:
public override string ToString()
{
return this.Name;
}
Create an instance of OrderDetail and set its properties. Create ListViewItem giving it the OrderDetail instance and add to the ListView. Repeat for as many OrderDetail instances you want.
Cheers -
Added: code which works:
int id = 12;
string rNameBoxText = "rName";
DateTime dateBoxValue = DateTime.Now;
string orderBoxText = "order";
string[] orderDetails = { Convert.ToString(id + 1), rNameBoxText, dateBoxValue.ToString(), orderBoxText };
//DEBUGGING
Console.WriteLine(orderDetails[0]);
Console.WriteLine(orderDetails[1]);
Console.WriteLine(orderDetails[2]);
Console.WriteLine(orderDetails[3]);
//END DEBUGGING
this.listView1.Columns.Clear();
this.listView1.Columns.Add("Id");
this.listView1.Columns.Add("rName");
this.listView1.Columns.Add("Date");
this.listView1.Columns.Add("Order");
this.listView1.View = View.Details;
//Add the order info to the ListView item on the main form
var listViewItem = new ListViewItem(orderDetails);
this.listView1.Items.Add(listViewItem);
We have a custom collection of objects that we bind to a listbox control. When an item is added to the list the item appears in the listbox, however when one selects the item the currency manager position will not go to the position. Instead the currency manager position stays at the existing position. The listbox item is high lighted as long as the mouse is press however the cm never changes position.
If I copy one of the collection objects the listbox operates properly.
One additional note the collection also has collections within it, not sure if this would be an issue.
I found the issue, after spending way too much time....
This issue was related to one of the propertys of the item(custom class) in the collection which was bound to a date picker control. The constructor for the class never set the value to a default value.
This caused an issue with the currency manager not allowing the position to change as the specific property (bound to the date picker) was not valid.
Me bad! I know better!
You might need to post some code; the following (with two lists tied together only by the CM) shows that it works fine... so to find the bug we might need some code.
using System;
using System.ComponentModel;
using System.Windows.Forms;
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
BindingList<Foo> foos = new BindingList<Foo>();
foos.Add(new Foo("abc"));
foos.Add(new Foo("def"));
ListBox lb1 = new ListBox(), lb2 = new ListBox();
lb1.DataSource = lb2.DataSource = foos;
lb1.DisplayMember = lb2.DisplayMember = "Bar";
lb1.Dock = DockStyle.Left;
lb2.Dock = DockStyle.Right;
Button b = new Button();
b.Text = "Add";
b.Dock = DockStyle.Top;
b.Click += delegate
{
foos.Add(new Foo("new item"));
};
Form form = new Form();
form.Controls.Add(lb1);
form.Controls.Add(lb2);
form.Controls.Add(b);
Application.Run(form);
}
}
class Foo
{
public Foo(string bar) {this.Bar = bar;}
private string bar;
public string Bar {
get {return bar;}
set {bar = value;}
}
}
Collections don't have a sense of "current item". Perhaps your custom collection does, but the ListBox is not using that. It has its own "current item" index into the collection. You need to handle SelectedIndexChanged events to keep them in sync.