views:

849

answers:

2

Hey guys. I'm working in WinForms and trying to create a ComboBox control that hosts a ListView when it's dropped down. I'm using ToolStripDropDown and ToolStripControlHost to accomplish this.

My problem is that i cannot display selected ListView item in the ComboBox when i set its DropDownStyle to DropDownList. Obviously this behavior only works when the actual ComboBox contains items in its collection. I ended up manually adding items to the ComboBox in my OnDoubleClick event and settings its selected index to 0 to display the item. But this still doesn't work. I'm out of ideas.

This is my ListViewComboBox class

using System.ComponentModel;
using System.Drawing;
using System.Security.Permissions;
using System.Windows.Forms;

namespace WindowsFormsApplication2
{
    [ToolboxItem(true)]
    [SecurityPermissionAttribute(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
public class ListViewComboBox : ComboBox
{
    private ToolStripControlHost _controlHost;
    private ToolStripDropDown _dropDown;

    public ListViewComboBox()
    {
        ListView innerListView = new ListView();
        innerListView.BorderStyle = BorderStyle.None;
        innerListView.View = View.SmallIcon;
        innerListView.MultiSelect = false;
        innerListView.HoverSelection = true;
        innerListView.DoubleClick += new System.EventHandler(listView_DoubleClick);

        _controlHost = new ToolStripControlHost(innerListView);
        _controlHost.AutoSize = false;

        _dropDown = new ToolStripDropDown();
        _dropDown.Items.Add(_controlHost);
    }

    public void AutoSizeItems()
    {
        foreach (ListViewItem item in Items)
        {
            Size size = TextRenderer.MeasureText(item.Text, item.Font);
            ListView lstView = (ListView)_controlHost.Control;

            if (size.Width >= this.DropDownWidth)
            {
                this.DropDownWidth = size.Width + 40;
            }
        }
    }

    void listView_DoubleClick(object sender, System.EventArgs e)
    {
        if (sender is ListView)
        {
            ListView control = (ListView)sender;

            base.Items.Clear();

            if (control.SelectedItems.Count > 0)
            {
                base.Items.Add(control.SelectedItems[0].Text);
                base.Text = control.SelectedItems[0].Text;
                this.SelectedIndex = 0;
                this.Focus();
            }

            _controlHost.PerformClick();
        }
    }

    public new ListView.ListViewItemCollection Items
    {
        get
        {
            return ((ListView)_controlHost.Control).Items;
        }
    }

    public ListViewGroupCollection Groups
    {
        get
        {
            return ((ListView)_controlHost.Control).Groups;
        }
    }

    public new ListViewItem SelectedItem
    {
        get
        {
            return ((ListView)_controlHost.Control).SelectedItems[0];
        }
    }

    private void ShowDropDown()
    {
        if (_dropDown != null)
        {
            _controlHost.Width = this.DropDownWidth;
            _controlHost.Height = this.DropDownHeight;

            _dropDown.Show(this, 0, this.Height);
            _controlHost.Focus();
        }
    }

    private const int WM_USER = 0x0400,
                      WM_REFLECT = WM_USER + 0x1C00,
                      WM_COMMAND = 0x0111,
                      CBN_DROPDOWN = 7,
                      LVM_FIRST = 0x1000,
                      LVM_SETCOLUMNWIDTH = (LVM_FIRST + 30);

    public static int HIWORD(int n)
    {
        return (n >> 16) & 0xffff;
    }

    protected override void WndProc(ref Message m)
    {
        if (m.Msg == (WM_REFLECT + WM_COMMAND))
        {
            if (HIWORD((int)m.WParam) == CBN_DROPDOWN)
            {
                ShowDropDown();
                return;
            }
        }
        base.WndProc(ref m);
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (_dropDown != null)
            {
                _dropDown.Dispose();
                _dropDown = null;
            }
        }
        base.Dispose(disposing);
    }
}
}

This is how i'm using the above control:

    public Form1()
    {
        InitializeComponent();

        ListViewItem item = new ListViewItem();
        item.Font = new Font(item.Font.FontFamily.Name, 9, FontStyle.Bold);
        item.ForeColor = Color.MediumBlue;
        item.Text = "Test Entry";
        listViewComboBox1.Items.Add(item);
        listViewComboBox1.AutoSizeItems();
    }
+1  A: 

http://www.codeproject.com/KB/static/DropDownContainer.aspx

mangokun
The solution above uses tool strip controls to paint the entire combo box. I'm attempting to leverage the existing ComboBox control and not recreate its behavior. Thanks for the link though.
Sergey
A: 

The issue had to do with setting the draw-mode on the control to owner-draw. If anyone's interested, here is what the final control looks like. The solution is not really the final implementation, more of a proof of concept. It needs major refactoring to better mimic the functionality of the drop-down control and be generic enough to host multiple controls. But i'll leave it for later.

[ToolboxItem(true)]
[SecurityPermissionAttribute(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
public class ListViewComboBox : ComboBox
{
    #region Fields

    /// <summary>
    /// Used as the list view host.
    /// </summary>
    private readonly ToolStripControlHost _controlHost;

    /// <summary>
    /// Used as the pop-up window.
    /// </summary>
    private ToolStripDropDown _dropDown;

    /// <summary>
    /// Search input area.
    /// </summary>
    private ToolStripTextBox _searchBox;

    /// <summary>
    /// Contains tooltip instance used to show tooltip over list view item.
    /// </summary>
    private ToolTip _toolTip;

    /// <summary>
    /// Indicates whether the list view has been dropped down.
    /// </summary>
    private bool _dropped;

    /// <summary>
    /// Indicates whether the list view has been auto-sized.
    /// </summary>
    private bool _autoSized;

    #endregion

    #region Constructors

    /// <summary>
    /// Creates a new instance of <see cref="ListViewComboBox"/>.
    /// </summary>
    public ListViewComboBox()
    {
        ListView innerListView = new ListView();
        innerListView.Columns.Add("Vendors");
        innerListView.HeaderStyle = ColumnHeaderStyle.None; // Hide column headers
        innerListView.BorderStyle = BorderStyle.None;
        innerListView.View = View.Details; // 1 item per line
        innerListView.MultiSelect = false;
        innerListView.FullRowSelect = true;
        innerListView.HideSelection = false;
        innerListView.ShowGroups = true;
        innerListView.ShowItemToolTips = false;
        innerListView.DoubleClick += new System.EventHandler(ListView_DoubleClick);
        innerListView.ItemMouseHover += new ListViewItemMouseHoverEventHandler(ListView_ItemMouseHover);

        // Default height for now
        DropDownHeight = 300;

        _controlHost = new ToolStripControlHost(innerListView);
        _controlHost.AutoSize = false;

        _toolTip = new ToolTip();

        _searchBox = new ToolStripTextBox();
        _searchBox.BorderStyle = BorderStyle.FixedSingle;
        _searchBox.Dock = DockStyle.Fill;
        _searchBox.Text = "Search";
        _searchBox.Width = Width;
        _searchBox.TextChanged += new EventHandler(SearchBox_TextChanged);
        _searchBox.LostFocus += new EventHandler(SearchBox_LostFocus);
        _searchBox.GotFocus += new EventHandler(SearchBox_GotFocus);

        _dropDown = new ToolStripDropDown();
        _dropDown.DropShadowEnabled = false;
        _dropDown.Items.Add(_searchBox);
        _dropDown.Items.Add(_controlHost);
    }

    #endregion

    #region Event Handlers

    /// <summary>
    /// Selects the item clicked in the list view and updates
    /// combo box to display the selected item's text value.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void ListView_DoubleClick(object sender, EventArgs e)
    {
        if (sender is ListView)
        {
            ListView control = (ListView)sender;

            DataSource = null;

            if (control.SelectedItems.Count > 0)
            {
                // Bind the first selected item to the combo box
                // to display the item's textual value
                IList<ListViewItem> source = new List<ListViewItem>(1);
                source.Add(control.SelectedItems[0]);

                DataSource = source;
                DisplayMember = "Text";
                Tag = control.SelectedItems[0].Tag;
                SelectedIndex = 0;
            }

            HideDropDown();
        }
    }

    /// <summary>
    /// Applies tooltip on item hover.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void ListView_ItemMouseHover(object sender, ListViewItemMouseHoverEventArgs e)
    {
        if (!string.IsNullOrEmpty(e.Item.ToolTipText))
        {
            _toolTip.SetToolTip(InnerControl, e.Item.ToolTipText);
        }
        else
        {
            _toolTip.SetToolTip(InnerControl, e.Item.Text);
        }
    }

    /// <summary>
    /// Clears search box when focus is set.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void SearchBox_GotFocus(object sender, EventArgs e)
    {
        _searchBox.Clear();
    }

    /// <summary>
    /// Titles the search box when focus is lost.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void SearchBox_LostFocus(object sender, EventArgs e)
    {
        _searchBox.Text = "Search";
    }

    /// <summary>
    /// Performs search on search box text change.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void SearchBox_TextChanged(object sender, EventArgs e)
    {
        ListViewItem item = InnerControl.FindItemWithText(_searchBox.Text);

        if (item != null)
        {
            InnerControl.SelectedItems.Clear();
            InnerControl.EnsureVisible(item.Index);
            item.Selected = true;
            item.Focused = true;
            InnerControl.Select();
        }
    }

    #endregion

    #region Properties

    /// <summary>
    /// Provides access to the item collection.
    /// </summary>
    public new ListView.ListViewItemCollection Items
    {
        get
        {
            return InnerControl.Items;
        }
    }

    /// <summary>
    /// Provides access to the group collection.
    /// </summary>
    public ListViewGroupCollection Groups
    {
        get
        {
            return InnerControl.Groups;
        }
    }

    /// <summary>
    /// Gets selected item.
    /// </summary>
    public new ListViewItem SelectedItem
    {
        get
        {
            if (InnerControl.SelectedItems.Count > 0)
            {
                return InnerControl.SelectedItems[0];
            }

            return null;
        }
    }

    /// <summary>
    /// Gets selected item.
    /// </summary>
    public new object SelectedValue
    {
        get
        {
            return SelectedItem;
        }
    }

    /// <summary>
    /// Gets selected text.
    /// </summary>
    public new string SelectedText
    {
        get
        {
            if (SelectedItem != null)
            {
                return SelectedItem.Text;
            }

            return null;
        }
    }

    protected ListView InnerControl
    {
        get
        {
            return _controlHost.Control as ListView;
        }
    }

    #endregion

    #region Public Members

    /// <summary>
    /// Auto-sizes the drop-down based on the content lengths.
    /// </summary>
    public void AutoSizeItems()
    {
        AutoSizeInternal(InnerControl, false);
    }

    /// <summary>
    /// Sets an item in the list as selected.
    /// </summary>
    /// <param name="value">The item value to search for.</param>
    public void SetValue(string value)
    {
        ListViewItem item = InnerControl.FindItemWithText(value);

        if (item != null)
        {
            item.Selected = true;

            IList<ListViewItem> source = new List<ListViewItem>(1);
            source.Add(item);

            DataSource = source;
            DisplayMember = "Text";
            Tag = item.Tag;
            SelectedIndex = 0;
        }
    }

    /// <summary>
    /// Opens the drop-down.
    /// </summary>
    public void ShowDropDown()
    {
        if (_dropDown != null)
        {
            _controlHost.Width = DropDownWidth;
            _controlHost.Height = DropDownHeight;

            // Highlight selected item
            if (!string.IsNullOrEmpty(Text))
            {
                ListViewItem foundItem = InnerControl.FindItemWithText(Text);

                if (foundItem != null)
                {
                    foundItem.Selected = true;
                    foundItem.EnsureVisible();
                }
                else
                {
                    // Clear items which were clicked, but not selected
                    if (InnerControl.SelectedItems.Count > 0)
                    {
                        InnerControl.SelectedItems.Clear();
                    }
                }
            }

            _dropDown.Show(this, 0, Height);
            _controlHost.Focus();
            _dropped = true;
        }
    }

    /// <summary>
    /// Collapses the drop-down.
    /// </summary>
    public void HideDropDown()
    {
        if (_dropDown != null)
        {
            _dropDown.Hide();
            _dropped = false;
        }
    }

    #endregion

    #region Protected Members

    /// <summary>
    /// Process Win32 loop messages.
    /// </summary>
    /// <param name="m">The message to process.</param>
    protected override void WndProc(ref Message m)
    {
        if (m.Msg == (NativeMethods.WM_REFLECT + NativeMethods.WM_COMMAND))
        {
            if (NativeMethods.HIWORD((int)m.WParam) == NativeMethods.CBN_DROPDOWN)
            {
                // Prevent ComboBox from dropping down its list
                RecreateHandle();
                NativeMethods.SendMessage(Handle, NativeMethods.CB_SHOWDROPDOWN, 0, 0);

                // Autosize the list view
                if (!_autoSized)
                {
                    AutoSizeItems();
                    _autoSized = true;
                }

                // Show/Hide dropped down based on current state
                if (!_dropped)
                {
                    ShowDropDown();
                }
                else
                {
                    HideDropDown();
                }

                return;
            }
        }

        base.WndProc(ref m);
    }

    /// <summary>
    /// Disposes the control and releases memory.
    /// </summary>
    /// <param name="disposing"></param>
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (_dropDown != null)
            {
                _dropDown.Dispose();
                _dropDown = null;
            }
        }

        if (_toolTip != null)
        {
            _toolTip.Dispose();
            _toolTip = null;
        }

        base.Dispose(disposing);
    }

    #endregion

    #region Helper Members

    private void AutoSizeInternal(ListView lv, bool adjustForVerticalScrollBar)
    {
        int resizeByContent = 0;
        int resizeByHeader = 0;

        for (int i = 0; i < lv.Columns.Count; i++)
        {
            lv.Columns[i].AutoResize(ColumnHeaderAutoResizeStyle.ColumnContent);
            resizeByContent = lv.Columns[i].Width;

            lv.Columns[i].AutoResize(ColumnHeaderAutoResizeStyle.HeaderSize);
            resizeByHeader = lv.Columns[i].Width;

            lv.Columns[i].Width = (resizeByHeader > resizeByContent) ? resizeByHeader : resizeByContent;

            DropDownWidth = lv.Columns[i].Width + 20;

            if (DropDownWidth < Width)
            {
                DropDownWidth = Width;
            }
        }
    }

    #endregion
}

#region Native Methods Helper

internal static class NativeMethods
{
    #region Win32 API Stubs

    [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
    public static extern int SendMessage(IntPtr handle, int message, int wparam, int lparam);

    #endregion

    #region Constants

    internal const int WM_USER = 0x0400,
                       WM_REFLECT = WM_USER + 0x1C00,
                       WM_COMMAND = 0x0111,
                       WM_PAINT = 0xf,
                       CBN_DROPDOWN = 7,
                       CB_SHOWDROPDOWN = 0x14F;

    #endregion

    internal static int HIWORD(int n)
    {
        return (n >> 16) & 0xffff;
    }
}

#endregion
Sergey

related questions