0

This should be a duplicate question but I'm posting it because none of the answers anywhere are working.

I have a dictionary of the types:

private Dictionary<IModule, AssemblyLoadContext> ModuleList = new Dictionary<IModule, AssemblyLoadContext>();

I am trying to bind the names of the IModules (IModule.Handle, for everything that implements IModule) to a combobox.

I've tried many things and searched through every answer on google but nothing works. This is apparently the way you are supposed to do it:

comboBox1.DataSource = new BindingSource(ModuleList, null);
comboBox1.DisplayMember = "Value";
comboBox1.ValueMember = "Key";

When I do this I get a RUNTIME error: (System.ArgumentException: 'Cannot bind to the new display member. (Parameter 'newDisplayMember')' )

When I try swapping key and value I get this same error: (System.ArgumentException: 'Cannot bind to the new display member. (Parameter 'newDisplayMember')' )

When I try other combinations of key/value, I get random results. Sometimes it will show the entire class name (not helpful), sometimes it will show the ToString representation (overloaded and works perfectly except doesn't UPDATE after startup), and sometimes it just shows nothing or the program gives an error during runtime.

However no combination of things I have tried actually gets the BOX contents to UPDATE when modules are loaded and unloaded (the modules themselves are definitely loading/unloading and work fine).

This is supposedly working as of many years ago and I can only imagine microsoft broke something in one of their updates because the intended method does NOT work for me.

This is using .NET core 3.1 modules and .NET 5.0 application (required in order for modules to work because microsoft 5.0 exe does not work with microsoft 5.0 dll).

The overloaded ToString method of IModule returns Handle which is a string that names the module, IE "ConsoleModule", and works as intended. Everything else is working except the data binding.

Can anyone else at least confirm this data binding method actually works in .NET 5.0 and/or 3.1? Rapidly losing sanity.

Plaje
  • 65
  • 9
  • Do you mean that you're setting the DataSource of the BindingSource to an empty Dictionary, the you try to add Items to the Dictionary and you don't see the internal List change? -- Note that a Dictionary is a *Complex Object*, to use it as data source it's converted to a `List>`. This list doesn't support list change events. -- If you change the content of the Dictionary, you have to reset the BindingSource, set the DataSource of you ComboBox to `null`, set `DisplayMember` (first), `ValueMember` (second) and `DataSource` (last) again. – Jimi Jul 27 '21 at 18:22
  • Or choose a source of data that is not a *dynamic* Dictionary. – Jimi Jul 27 '21 at 18:22
  • I'm a bit confused then because I thought that was the entire point of databinding - the control reacts to changes. What is the point of data binding otherwise? In my case I need to use a dictionary for the remaining functionality. If dictionary is not intended to be used as a binding source then I guess I can understand the failure of this to work. I did a workaround which involves just updated the data every time a modification is made to the dictionary and it works that way for now, but my understanding was that you should be able to bindingsource a dictionary. – Plaje Jul 27 '21 at 18:33
  • 1
    You cannot directly set a Dictionary as the DataSource of a ComboBox. You have to set `[ComboBox].DataSource = [Dictionary].ToList();`. The same applies to a BindingSource, which creates a BindingList internally. A `List` where the class contains Properties of Type `IModule` and `AssemblyLoadContext` is probably easier to handle. – Jimi Jul 27 '21 at 18:36
  • Understood, but other users say this works for them (using the shown bindingsource): https://stackoverflow.com/questions/6412739/binding-combobox-using-dictionary-as-the-datasource In their instance the answer is marked as correct and (while not conclusive evidence) has a fair good amount of upvotes as if the answer were helpful. Here in my application this does not work. I had tried to do a class with two properties earlier but it made the program very difficult to work with without the convenience of the dictionary, as I would end up rewriting the dictionary class. – Plaje Jul 27 '21 at 19:25
  • 1
    Yes, that of course works. The Dictionary is filled and it's set to the DataSource of a ComboBox using a BindingSource as *mediator* (`DisplayMember` and `ValueMember` should be set before `DataSource`, but that's another matter). -- Different situation if you **don't** fill the Dictionary right away (so, it's empty) and/or you try to add KeyValuePairs after. In this case, you could initialize the BindingSource with the Type (e.g., `[BindingSource] = new BindingSource(typeof(Dictionary), null);)` - if needed - and reset it after you have added some items. – Jimi Jul 27 '21 at 19:35
  • I guess I am not understanding the thread then. My understanding is that the OP of that thread wants databinding to his dictionary. It sounds like what he is really doing according to your explanation is that he didn't really bind his datasource, he just populated the items in the databox? If so the thread is a bit misleading, although that does seem to be the case from my experiences today. – Plaje Jul 27 '21 at 19:56
  • 1
    Yep, it's actually bound to a `BindingList>`. If you add items to the Dictionary after, nothing happens: the ComboBox content remains the same. -- Note that you can add/remove Items (of Type `KeyValuePair`) to/from the `[BindingSource].List` property: this will add items to the ComboBox, but won't affect the Dictionary. – Jimi Jul 28 '21 at 10:52
  • Thanks Jimi, if you want you can add the information we talked about to an answer and I'll check it off. – Plaje Jul 28 '21 at 11:19

1 Answers1

1

Whenever you have a sequence of similar items, that you want to show in a ComboBox, you need to tell the ComboBox which property of the items should be used to display each item. You were right, this is done using ComboBox.DisplayMember

Your Dictionary<IModule, AssemblyLoadContext> implements IEnumerable<KeyValuePair<IModule, AssemblyLoadContext>, so you can regard it as if it is a sequence of KeyValuePairs. Every KeyValuePair has a Key of type IModule, and a Value of type AssemblyLoadContext.

The IModule and the AssemblyLoadContext have several properties. You need to decide which property of them you want to show.

I am trying to bind the names of the IModules (IModule.Handle)

I guess that every IModule has a property Handle, and you want to display this Handle in the ComboBox.

comboBox1.DisplayMember = nameof(IModule.Handle);

If you need a display only, so no updates, it is enough to convert your original sequence into a list:

Dictionary<IModule, AssemblyLoadContext> myData = ...
comboBox.DataSource = myData.ToList();

However, if you want to update the displayed data, you need an object that implements IBindingList, like (surprise!) BindingList<T>. See BindingList.

You can make a BindingList<KeyValuePair<IModule, AssemblyLoadContext>>, but this is hard to read, hard to understand, difficult to unit test, difficult to reuse and maintain. My advice would be to create a special class for this.

I haven't got a clue what's in the IModule, so you'll have to find a proper class name. I'll stick with:

class DisplayedModule
{
    public string DisplayText => this.Module.Handle;

    public IModule Module {get; set;}
    public AssemblyLoadContext AssemblyLoadContext{get; set;}
}

And in the constructor of your form:

public MyForm()
{
    InitializeComponent();

    this.ComboBox1.DisplayMember = nameof(DisplayedModule.DisplayText);

This way, if you want to change the text that needs to be displayed, all you have to do is change property DisplayText.

public BindingList<DisplayedModule> DisplayedItems
{
    get => (BindingList<DisplayedModule>)this.comboBox1.DataSource;
    set => this.comboBox1.DataSource = value;
}

You need procedures to get the initial data:

private Dictionary<IModule, AssemblyLoadContext> GetOriginalData() {...} // out of scope of this question

private IEnumerable<DisplayedModule> OriginalDataToDisplay =>
    this.GetOriginalData().Select(keyValuePair => new DisplayedModule
    {
        Module = keyValuePair.Key,
        AssemblyLoadcontext =  keyValuePair.Value;
    });

I have put this in separate procedures, to make it very flexible. Easy to understand, easy to unit test, easy to change and to maintain. If for instance your Original data is not in a Dictionary, but in a List, or an Array, or from a database, only one procedure needs to change.

To initially fill the comboBox is now a one-liner:

private ShowInitialComboBoxData()
{
    this.DisplayedItems = new BindingList<DisplayedModule>
        (this.OriginalDataToDisplay.ToList());
}

private void OnFormLoad(object sender, ...)
{
    this.ShowInitialComboBoxData();
    ... // other inits during load form
}

If the operator adds / removed an element to the list, the bindinglist is automatically updated. If something happens, after which you know that the dictionary has been changed, you can simply change the bindingList For small lists that do not change often, I would make a complete new BindingList. If the List changes often, or it is a big list, consider to Add / Remove the original BindingList.

private void AddDisplayedModule(DisplayedModule module)
{
    this.DisplayedItems.Add(module);
}

private void RemoveDisplayedMOdule(DisplayedModule module)
{
    this.DisplayedItems.Remove(module);
}

private void ModuleAddedToDictionary(IModule module, AssemblyLoadContext assembly)
{
    this.AddDisplayedModule(new DisplayedModule
    {
        Module = module,
        AssemblyLoadContext = assembly,
    })
}

If the operator makes some changes, and indicates he finished editing the comboBox, for instance by pressing the "Apply Now" button, you can simply get the edited data:

private void ButtonApplyNowClicked(object sender, ...)
{
    // get the edited data from the combobox and convert to a Dictionary:
    Dictionary<IModule, AssemblyLoadContext> editedData = this.DisplayedItems
        .ToDictionary(displayedItem => displayedItem.Module,               // Key
                      displayedItem => displayedItem.AssemblyLoadContext); // Value;
    this.ProcesEditedData(editedData);
}

To access the Selected item of the comboBox

DisplayedModule SelectedModule => (DisplayedModule)this.comboBox1.SelectedItem;

Conclusion

By separating you data from the way that it is displayed, changes will be minimal if you decide to change your view: change Combobox into a ListBox, or even a DataGridView. Or if you decide to change your data: not a Dictionary, but a sequence from a Database

Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116
  • I was going to mark this as the answer but comboBox1.DisplayMember = nameof(IModule.Handle); is displaying the class info instead of the Handle string, for some reason. Any ideas why? I don't have time to try all the stuff you listed as the current implementation (manually updating the combobox) is working OK for now, but I'd like to have it managed by the language like I should be doing. Problem is the program heavily relies on the Handle being accurate in order to function, and gets the unload Handle name from the combobox. – Plaje Jul 29 '21 at 11:58