简体   繁体   中英

Using a ComboBox DropDownList to enumerate COM ports keeps losing current selection in the DropDown event due to clear()

There is a function that automatically renews the serialPort in the DropDown event. That's why we periodically clear() it. First {COM1, COM2, COM3}, then {COM1, COM2} when disconnected, maybe {COM1, COM2, COM3} when reconnected.

The problem is disappear that a previously selected value whenever an event occurs. Someone points out that it is because of the use of clear() . But, it doesn't occur this problem when DropDownStyle is DropDown .

I wrote the following code to make the comboBox1 ReadOnly .

comboBox1.DropDownStyle = ComboBoxStyle.DropDownList;

And, I also have the code like this:

    private void comboBox1_DropDown(object sender, EventArgs e)
    {
        comboBox1.Items.Clear();
        // Logic to automatically add items to comboBox1
        .
        .
        .
    }

How do I should solve this problem? The key is that it can't input in comboBox other than user selecting value.

Here is an alternative to using Clear .

One solution is to have a binding list that is the data source of your ComboBox. When you enumerate the available COM ports (shown below as a simulation) you will add or remove com ports from the binding source based on availability. This should result in the behavior you want, because if a com port is "still" available the selection won't change.

截屏

public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();
        comboBoxCom.DropDownStyle= ComboBoxStyle.DropDownList;
        comboBoxCom.DataSource = MockDataSource;
        refreshAvailableComPorts(init: true);
        comboBoxCom.DropDown += onComboBoxComDropDown;
    }
    BindingList<string> MockDataSource = new BindingList<string>();

    private void onComboBoxComDropDown(object? sender, EventArgs e)
    {
        refreshAvailableComPorts(init: false);
    }

    /// <summary>
    /// This is a simulation that will choose a different
    /// set of "available" ports on each drop down.
    /// </summary>
    private void refreshAvailableComPorts(bool init)
    {
        if(init) 
        {
            MockDataSource.Clear();
            MockDataSource.Add("COM2");
        }
        else
        {
            string 
                selB4 = string.Join(",", MockDataSource),
                selFtr;
            do
            {
                var flags = _rando.Next(1, 0x10);
                if ((flags & 0x1) == 0) MockDataSource.Remove("COM1");
                else if(!MockDataSource.Contains("COM1")) MockDataSource.Add("COM1");
                if ((flags & 0x2) == 0) MockDataSource.Remove("COM2");
                else if (!MockDataSource.Contains("COM2")) MockDataSource.Add("COM2");
                if ((flags & 0x4) == 0) MockDataSource.Remove("COM3");
                else if (!MockDataSource.Contains("COM3")) MockDataSource.Add("COM3");
                if ((flags & 0x8) == 0) MockDataSource.Remove("COM4");
                else if (!MockDataSource.Contains("COM4")) MockDataSource.Add("COM4");
                selFtr = string.Join(",", MockDataSource);
                if (!selFtr.Equals(selB4))
                {
                    break;
                }
            } while (true); 
        }
    }
    private Random _rando = new Random(5);
}

I don't make a habit of posting multiple answers on the same thread, but based on your helpful comment, it appears that your "underlying" reason for posting is wanting to keep the ComboBox up to date with COM ports that are currently connected.

There might be a more optimal solution to the thing you were trying to do in the first place (this is known as an XY Problem ). In my old job I used to do serial port and USB enumeration quite a bit. One trick I learned along the way is that WinOS is going to post a message WM_DEVICECHANGE whenever a physical event occurs, things like plugging in or disconnecting a USB serial port device.

In a WinForms app, it's straightforward to detect it by overriding WndProc :

public MainForm() => InitializeComponent();
const int WM_DEVICECHANGE = 0x0219;
protected override void WndProc(ref Message m)
{
    base.WndProc(ref m);
    if(m.Msg == WM_DEVICECHANGE) 
    {
        onDeviceChange();
    }
}

BindingList

I would still advocate for assigning the DataSource property of the combo box to a BindingList<ComPort> where ComPort is a class we design to represent the connection info:

class ComPort
{
    public string? PortName { get; set; }
    public string? FriendlyName { get; set; }
    public string? PnpDeviceId { get; set; }
    public override string ToString() => PortName ?? "None";
    public string GetFullDescription()
    {
        var builder = new List<string>();
        builder.Add($"{PortName}");
        builder.Add($"{FriendlyName}");
        builder.Add($"{nameof(PnpDeviceId)}: ");
        builder.Add($"{PnpDeviceId}");
        return $"{string.Join(Environment.NewLine, builder)}{Environment.NewLine}{Environment.NewLine}";
    }
}

Enumeration

There's a little extra work this way but the enumeration yields a descriptor with additional useful information:

截屏

    BindingList<ComPort> ComPorts = new BindingList<ComPort>();

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        comboBoxCom.DropDownStyle = ComboBoxStyle.DropDownList;
        comboBoxCom.DropDownClosed += (sender, e) => ActiveControl = null;
        comboBoxCom.DataSource = ComPorts;
        ComPorts.ListChanged += onComPortChanged;
        enumerateComPorts();
    }
    bool _isDeviceChange = false;

    private void onComPortChanged(object? sender, ListChangedEventArgs e)
    {
        if(_isDeviceChange) BeginInvoke(() => richTextBox.Clear());
        ComPort comPort;
        switch (e.ListChangedType)
        {
            case ListChangedType.ItemAdded:
                comPort = ComPorts[e.NewIndex];
                using (ManagementClass manager = new ManagementClass("Win32_PnPEntity"))
                {
                    foreach (ManagementObject record in manager.GetInstances())
                    {
                        var pnpDeviceId = record.GetPropertyValue("PnpDeviceID")?.ToString();
                        if (pnpDeviceId != null)
                        {
                            var subkey = Path.Combine("System", "CurrentControlSet", "Enum", pnpDeviceId, "Device Parameters");
                            var regKey = Registry.LocalMachine.OpenSubKey(subkey);
                            if (regKey != null)
                            {
                                var names = regKey.GetValueNames();
                                if (names.Contains("PortName"))
                                {
                                    var portName = regKey.GetValue("PortName");
                                    if (Equals(comPort.PortName, portName))
                                    {
                                        var subkeyParent = Path.Combine("System", "CurrentControlSet", "Enum", pnpDeviceId);
                                        var regKeyParent = Registry.LocalMachine.OpenSubKey(subkeyParent);
                                        comPort.FriendlyName = $"{regKeyParent?.GetValue("FriendlyName")}";
                                        comPort.PnpDeviceId = pnpDeviceId;
                                    }
                                }
                            }
                        }
                    }
                }
                break;
            case ListChangedType.ItemDeleted:
                comPort = _removedItem!;
                break;
            default: return;
        }
        BeginInvoke(() =>
        {
            if (_isDeviceChange) 
            {
                richTextBox.SelectionColor =
                     e.ListChangedType.Equals(ListChangedType.ItemAdded) ?
                     Color.Green : Color.Red;
                richTextBox.AppendText($"{e.ListChangedType}{Environment.NewLine}");
            }
            else
            {
                richTextBox.SelectionColor = Color.Blue;
                richTextBox.AppendText($"Detected{Environment.NewLine}");
            }
            richTextBox.SelectionColor = Color.FromArgb(0x20, 0x20, 0x20);
            richTextBox.AppendText(comPort?.GetFullDescription());
        });
    }

    int _wdtCount = 0;
    private ComPort? _removedItem = null;

    private void onDeviceChange()
    {
        _isDeviceChange = true;
        int captureCount = ++_wdtCount;
        Task
            .Delay(TimeSpan.FromMilliseconds(500))
            .GetAwaiter()
            .OnCompleted(() =>
            {
                if(captureCount.Equals(_wdtCount))
                {
                    // The events have settled out
                    enumerateComPorts();
                }
            });
    }
    private void enumerateComPorts()
    {
        string[] ports = SerialPort.GetPortNames();
        foreach (var port in ports)
        {
            if (!ComPorts.Any(_ => Equals(_.PortName, port)))
            {
                ComPorts.Add(new ComPort { PortName = port });
            }
        }
        foreach (var port in ComPorts.ToArray())
        {
            if (!ports.Contains(port.PortName))
            {
                _removedItem = port;
                ComPorts.Remove(port);
            }
        }
        BeginInvoke(()=> ActiveControl = null); // Remove focus rectangle
    }

The DropDown event of the combo box is now no longer required. Problem solved!

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