#region License // Copyright (c) 2013, ClearCanvas Inc. // All rights reserved. // http://www.clearcanvas.ca // // This file is part of the ClearCanvas RIS/PACS open source project. // // The ClearCanvas RIS/PACS open source project is free software: you can // redistribute it and/or modify it under the terms of the GNU General Public // License as published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // The ClearCanvas RIS/PACS open source project is distributed in the hope that it // will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General // Public License for more details. // // You should have received a copy of the GNU General Public License along with // the ClearCanvas RIS/PACS open source project. If not, see // <http://www.gnu.org/licenses/>. #endregion using System; using System.Collections; using System.Windows.Forms; using System.ComponentModel; using ClearCanvas.Common.Utilities; namespace ClearCanvas.Desktop.View.WinForms { /// <summary> /// Special combo-box that works with a <see cref="ISuggestionProvider"/> to populate the drop-down list /// with suggested items as the user types. /// </summary> public class SuggestComboBox : ComboBox { private ISuggestionProvider _suggestionProvider; private event EventHandler _valueChanged; #region Public properties /// <summary> /// Gets or sets the <see cref="ISuggestionProvider"/>. /// </summary> [Browsable(false)] public ISuggestionProvider SuggestionProvider { get { return _suggestionProvider; } set { if (_suggestionProvider != null) _suggestionProvider.SuggestionsProvided -= ItemsProvidedEventHandler; _suggestionProvider = value; if (_suggestionProvider != null) { _suggestionProvider.SuggestionsProvided += ItemsProvidedEventHandler; // Bug #2222: only call SetQuery if this is a "simple" dropdown, in which case // we need to populate the list immediately // calling it for other styles causes behaviour described in #2222, // and there is really no need to get suggestions until user actually types if (this.DropDownStyle == ComboBoxStyle.Simple) _suggestionProvider.SetQuery(this.Text); } } } /// <summary> /// Gets or sets the current value of the control. /// </summary> [Browsable(false)] public object Value { get { return this.SelectedItem; } set { if(!Equals(this.SelectedItem, value)) { // in order to set the value, the Items collection must contain the value // but if the value is null, can just use an empty list // (also need to check for DBNull, for some stupid reason) var items = new ArrayList(); if(value != null && value != DBNull.Value) { items.Add(value); } else { // if value is null or DBNull, clear the text this.Text = null; } UpdateListItems(items); this.SelectedItem = value; OnValueChanged(EventArgs.Empty); } } } /// <summary> /// Occurs when the <see cref="Value"/> property changes. /// </summary> [Browsable(false)] public event EventHandler ValueChanged { add { _valueChanged += value; } remove { _valueChanged -= value; } } #endregion #region Overrides and Helpers protected virtual void OnValueChanged(EventArgs args) { EventsHelper.Fire(_valueChanged, this, EventArgs.Empty); } protected override void OnCreateControl() { this.SelectedItem = null; base.OnCreateControl(); } protected override void OnSelectionChangeCommitted(EventArgs e) { // there are 2 ways that the value can change // either the selection change is comitted, or the control loses focus OnValueChanged(EventArgs.Empty); base.OnSelectionChangeCommitted(e); } protected override void OnLeave(EventArgs e) { UpdateSelectionFromText(); base.OnLeave(e); } // Defect #5876: SuggestComboxBox mishandles 'Esc' once input is resolved // Override this method to clear the text when the ESC key is pressed. // Otherwise the component hosting the combobox will eat the ESC key, // and close the component if the CancelButton is defined. protected override bool ProcessCmdKey(ref Message msg, Keys k) { if (k == Keys.Escape) { this.Text = null; UpdateSelectionFromText(); this.DroppedDown = false; return true; } return base.ProcessCmdKey(ref msg, k); } protected override void OnKeyDown(KeyEventArgs e) { switch (e.KeyCode) { case Keys.Escape: this.Text = null; OnSelectionChangeCommitted(e); break; default: break; } base.OnKeyDown(e); } protected override void OnTextUpdate(EventArgs e) { base.OnTextUpdate(e); CursorReset(); _suggestionProvider.SetQuery(this.Text); } protected override void Dispose(bool disposing) { if (disposing) { // setting this to null will also unsubscribe from the last subscribed-to provider this.SuggestionProvider = null; } base.Dispose(disposing); } private void ItemsProvidedEventHandler(object sender, SuggestionsProvidedEventArgs e) { // Remember the current text and selection start, // as they exists prior to modifying the items collection var curText = this.Text; var cursorPosition = this.SelectionStart; if (e.Items.Count == 0) { try { this.DroppedDown = false; // there are no suggestions, so clear the items list this.Items.Clear(); // reset text back to original text // and return the cursor to the original position this.Text = curText; this.SelectionStart = cursorPosition; } catch (ArgumentOutOfRangeException) { // just in case...it throws the exception while clearing the list } } else { // at least 1 suggestion exists // open the dropdown menu this.DroppedDown = true; // update list UpdateListItems(e.Items); // set cursor to the end this.Text = curText; this.SelectionStart = curText.Length; this.SelectionLength = 0; } } private void UpdateListItems(ICollection items) { this.Items.Clear(); if (items.Count > 0) { var array = new object[items.Count]; items.CopyTo(array, 0); this.Items.AddRange(array); } } #endregion private static void CursorReset() { Cursor.Current = Cursors.Default; Cursor.Show(); } private void UpdateSelectionFromText() { try { // do a case-insensitive search var itemIndex = this.FindStringExact(this.Text); if (itemIndex > -1) { // update the selected index this.SelectedIndex = itemIndex; // also update the visible text, because the upper/lower-casing may not match var item = this.Items[itemIndex]; this.Text = GetItemText(item); } else { // doesn't match any suggestions, clear the text this.Text = null; } } catch (ArgumentOutOfRangeException) { // if the combo box is dropped down, and the control loses focus (calling OnLeave), // it seems WinForms throws an exception from this.Text // not sure why this happens, but there is really nothing that can be done in terms of recovery } // there are 2 ways that the value can change // either the selection change is comitted, or the control loses focus OnValueChanged(EventArgs.Empty); } } }