简体   繁体   中英

Binding Dictionary to ComboBox in C# .net winforms

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.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM