Designing and Implement Lookup Control for Windows Forms
文/黃忠成
What's Lookup Control
前篇所開發的OrpButtonEdit控件,雖然已經達到了初步的需求,但使用這個控件,設計師仍然必須自行設計開出的查詢視窗、處理選取的資料、回填至ButtonEdit控件中等課題,然而這些動作都是可規格化的,本文中所開發的Lookup Control將針對此問題,做出更便利的選取資料介面。事實上,Lookup Control在很早期的商用應用程式就已出現,她是一個類似ComboBox的控件,只是拉出的視窗不僅僅顯示單欄資料,而是顯示出一個Grid,讓使用者可以看到一整筆資料,而非僅僅單一欄位,見圖1。
圖1
設計這樣的控件,有兩個不可缺的關鍵控件,一是DataGridView控件,用來顯示可選取的資料,二是Form控件,DataGridView控件必須存活於Container Control中,例如Panel或是Form,多數情況下,為了得到更大的控制權,3rd Patrt廠商多會選擇使用Form而非Panel,做為DataGridView控件的Container。
Requirement
Lookup Control的需求很簡單,其必須提供DataSource/DataMember等資料繫結所需的屬性,讓設計者設定欲顯示的資料,同時也必須提供一個Columns Collection,允許設計者選取欲列出的欄位。不過由於列出的欄位數量不等,所以可能會出現拉下的視窗太小,不足以顯示所有欄位的問題,因此,Lookup Control必須提供一個屬性,讓設計者可以設定拉下視窗的寬度,至於高度,就不需要設計者插手,由Lookup Control視目前視窗的高度來計算最佳顯示高度即可。
Problem
Lookup Control唯一會遭遇的技術困難是,Form在Windows Forms架構中屬於容器型控件,每個Form都是單獨的個體,而Lookup Control所拉出的Form,必須受控於Lookup Control所在的Form,也就是當Lookup Control所在的Form移動時,這個拉下的Form也要跟著移動,這個問題有兩種解法,一種是MDI介面,不過此種方法雖可達到目的,但卻會引發其它的問題,就控件角度來說,我們不應該要求放Lookup Control的Form一定要是MDI Parent,就UI角度而言,變成MDI介面後會有許多限制。因此能用的方法只剩一個,那就是Form所提供的AddOwnedForm函式,呼叫此函式將欲受此Form管轄的Form傳入,就可以解決此處所遭遇的問題。
Designing
曾看過『深入剖析 ASP.NET組件設計』一書的讀者,應該都還記得,我於該書中撰寫了一個WebComboBox控件,於其中加入了ItemBuilder概念,允許設計師以Plug-In的方式,改變下拉視窗中的內容。現在,我將這個概念運用於此Lookup Control中,讓Lookup Control的層級更抽象化,不僅可以拉下DataGridView控件,也可以拉下各式各樣的控件,圖2是此控件的設計圖。
圖2
這張設計圖中,披露了兩個主要的元素,一是OrpCustomEmbedControlEdit,這是一個繼承自OrpCustomButtonEdit的控件,她負責建立下拉視窗,也就是Form容器,並呼叫第二個元素:OrpEmbedEditControl來填入容器中的內容,OrpEmbedEditControl是一個元件,其定義如程式1。
程式1
public {
{
{
} }
[Category("Appearance")]
{
{
}
{ _clientFormWidth = value; } }
[Browsable(false)]
{
{
} }
{ _clientForm = clientForm; _editControl = editControl; }
{ EditControl.CloseClientForm(isCancel); } } |
如你所見,這是一個抽象類別,其中定義了InitializeControl、ParseValue、GetInputValue、ClientFormClosed等函式,當OrpCustomEmbedControlEdit啟動下拉動作時,會建立一個Form,然後呼叫InitializeControl函式,OrpEmbedEditControl必須在此將欲顯示於該下拉視窗中的控件填入,接著ParseValue函式會被呼叫,此處必須依據傳入的值,調整視窗的內容,讓使用者可以看到原本所選取的值,然後必須處理選取資料的動作,當使用者選取資料後,下拉視窗會被關閉,此時GetInputValue函式會被呼叫,其必須傳回使用者所選取的值,最後ClientFormClosed函式會被呼叫,此處可以進行視窗關閉後的後續工作,整個流程圖示如圖3。
圖3
Implement
完成了設計圖後,實作就不難了,OrpCustomEmbedControlEdit的工作在於建立下拉視窗,然後呼叫EmbedEditControl元件來填入內容物,這裡會遭遇到一個實作上的困擾,就是何時關閉視窗?這有幾種情況,一是使用者在拉下視窗後,又按下了下拉按鈕,此時自然得關閉視窗,這是Cancel模式,使用者選取的值不會填回OrpCustomEmbedControlEdit中。二是使用者於拉下視窗後,將焦點移到其它控件上,此時一樣視為Cancel模式,關閉視窗。三是使用者調整了含有OrpCustomEmbedControlEdit控件Form的大小,或是於其上點選了滑鼠,這一樣視為Cacnel模式。程式2為OrpCustomEmbedControlEdit的原始碼列表,讀者可於其中看到處理視窗何時開啟、何時關閉的程式碼。
程式2
[ToolboxItem(false)]
{
[Category("Appearance")]
{
{
}
{ _clientFormWidth = value; } }
{
{
_clientForm = CreateClientForm();
} }
{
{
} }
{
{
}
{ _embedEditControl = value; } }
{
}
{
{
{ ownerForm.MouseClick -= new ownerForm.Activated -= new ownerForm.Resize -= new ownerForm.RemoveOwnedForm(_clientForm); }
{
Text = (string)EmbedEditControl.GetInputValue(); EmbedEditControl.ClientFormClosed(); } _clientForm.Close(); _clientForm.Dispose(); _clientForm = null; } }
{
ClientForm.Location = new ClientForm.Width = Width; ClientForm.Height = Screen.PrimaryScreen.Bounds.Height - ClientForm.Top - 30; ClientForm.FormBorderStyle = FormBorderStyle.None; ClientForm.Font = (Font)Font.Clone(); ClientForm.BackColor = SystemColors.Window;
ClientForm.Height = 160; ClientForm.StartPosition = FormStartPosition.Manual; ClientForm.ShowInTaskbar = false;
{ ownerForm.AddOwnedForm(ClientForm); ownerForm.MouseClick += new ownerForm.Activated += new ownerForm.Resize += new }
ClientForm.Width = EmbedEditControl.ClientFormWidth;
ClientForm.Width = ClientFormWidth; }
{ CloseClientForm(true); }
{
CloseClientForm(true); }
{
{
_skipLostFocus = false;
CloseClientForm(true); _closeTime = DateTime.Now; } }
{ CloseClientForm(true); }
{
CloseClientForm(false);
{
{ _skipLostFocus = true; ShowClientForm();
{ EmbedEditControl.InitializeControl(ClientForm, this); EmbedEditControl.ParseValue(Text); } ClientForm.Visible = true; } } } } |
OrpCustomEmbedControlEdit控件不是一個可顯示於Toolbox Pattern上的控件,其繼承者:OrpEmbedControlEdit才是。
程式3
[ToolboxItem(true)]
{ [Category("Behavoir")]
{
{
}
{ EmbedEditControl = value; } } } |
Implement ComboBox
完成了OrpCustomEmbedControlEdit這個基底控件後,現在我們可以將焦點放在如何設計可用的EmbedEditControl元件:一個類似ComboBox的控件,她與一般的ComboBox控件不同的是,其內容是可以切換的,舉個例來說,設計師可以放一個OrpEmbedControlEdit控件到Form上,放兩個ListEmbedEditControl元件到Form上,此時該OrpEmbedControlEdit可以動態的切換要使用那個ListEmbedEditControl來顯示可選取的資料,如圖4。
圖4
聰明的你,是否看出OrpEmbedEditControl這個設計的真正意含?是的!可動態切換的下拉視窗內容,可以讓設計師只用一個控件,應對不同的情況。程式4是ListEmbedEditControl元件的原始碼。
程式4
using System; using System.Drawing.Design; using System.ComponentModel; using System.Collections; using System.Collections.Generic; using System.Text; using System.Windows.Forms;
namespace LookupComboBox { [TypeConverter(typeof(ListItemConverter)),
{
{
{
}
{ _text = value; } }
{
{
}
{ _value = value; } }
{ _text = text; _value = value; }
{ } }
[Serializable]
{
{
{
}
} }
{
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] [Category("Data")]
{
{
_items = new
} }
[AttributeProvider(typeof(IListSource))] [Category("Data")]
{
{
}
{
_dataSource = value; } }
[DefaultValue(""), TypeConverter("System.Windows.Forms.Design.DataMemberFieldConverter, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"), Editor("System.Windows.Forms.Design.DataMemberFieldEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))] [Category("Data")]
{
{
}
{ _displayMember = value; } }
[DefaultValue(""), TypeConverter("System.Windows.Forms.Design.DataMemberFieldConverter, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"), Editor("System.Windows.Forms.Design.DataMemberFieldEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))] [Category("Data")]
{
{
}
{ _valueMember = value; } }
{
_innerListBox = new _innerListBox.Click += new _innerListBox.KeyDown += new _innerListBox.Dock = DockStyle.Fill;
{
_innerListBox.Items.Add(item); _innerListBox.DisplayMember = "Text"; _innerListBox.ValueMember = "Value"; }
{ _innerListBox.DataSource = DataSource; _innerListBox.DisplayMember = DisplayMember; _innerListBox.ValueMember = ValueMember; } _innerListBox.BorderStyle = BorderStyle.Fixed3D; clientForm.Controls.Add(_innerListBox); }
{
CloseClientForm(false);
CloseClientForm(true); }
{ CloseClientForm(false); }
{
_innerListBox.SelectedIndex = index; }
{
{
}
}
{
{ _innerListBox.Click -= new _innerListBox.KeyDown -= new } } }
[ToolboxItem(true)]
{ [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] [Category("Data")]
{
{
} }
[AttributeProvider(typeof(IListSource))] [Category("Data")]
{
{
}
{ ((ListEmbedEditControl)EmbedEditControl).DataSource = value; } }
[DefaultValue(""), TypeConverter("System.Windows.Forms.Design.DataMemberFieldConverter, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"), Editor("System.Windows.Forms.Design.DataMemberFieldEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))] [Category("Data")]
{
{
}
{ ((ListEmbedEditControl)EmbedEditControl).DisplayMember = value; } }
[DefaultValue(""), TypeConverter("System.Windows.Forms.Design.DataMemberFieldConverter, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"), Editor("System.Windows.Forms.Design.DataMemberFieldEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))] [Category("Data")]
{
{
}
{ ((ListEmbedEditControl)EmbedEditControl).ValueMember = value; } }
: base() { EmbedEditControl = new } } } |
關於ListItems、DesignerSerializationVisibility及TypeConverter部份,請參考拙著:『深入剖析 ASP.NET組件設計』一書,此處就不再贅述。ListEmbedEditControl元件的重點只有一個,那就是InitializeControl函式,此處建立了一個ListBox控件,並放入由OrpCustomEmbedControlEdit所傳入的Form中,剩下的動作就是如何與其互動罷了,圖5是執行畫面。
你也可以使用前面所開發的OrpEmbedControlEdit控件,而非OrpComboBox(這是一個整合了OrpEmbedControlEdit控件及ListEmbedEditControl元件的控件),圖6是其設計時期畫面。
圖6
Implement LookupEdit
如果你可以看懂ListEmbedEditControl元件,那麼接下來的GridEmbedEditControl元件也就不難了,重點同樣在InitializeControl函式,只是從ListBox變成DataGridView而已。
程式5
using System; using System.Drawing; using System.Drawing.Design; using System.ComponentModel; using System.Collections; using System.Collections.Generic; using System.Text; using System.Windows.Forms;
namespace LookupComboBox { [TypeConverter(typeof(LookupColumnItemConverter)),
{
[NonSerialized]
{
{
}
{ _owner = value; } }
{
{
}
{ _header = value; } }
[TypeConverter(typeof(LookupColumnNameConverter))]
{
{
}
{ _displayMember = value;
Header = value; } }
{
{
}
{ _width = value; } }
{ _header = header; _displayMember = displayMember; _width = width; }
{ } }
[Serializable]
{
{
{
}
{
} }
{
{
} }
{ ((IList)this).Add(value); }
{
Add(item); }
{
((LookupColumnItem)value).Owner = this; }
: base() { _owner = owner; } }
{
[AttributeProvider(typeof(IListSource))] [Category("Data")]
{
{
}
{
_dataSource = value; } }
[TypeConverter(typeof(DataMemberConverter))] [Category("Data")]
{
{
}
{ _dataMember = value; } }
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] [Category("Data")]
{
{
_items = new
} }
{
_bindingSource = new _bindingSource.DataSource = _dataSource; _bindingSource.DataMember = _dataMember; _gridView = new _gridView.AutoGenerateColumns = false; _gridView.AllowUserToAddRows = false; _gridView.AllowUserToDeleteRows = false; _gridView.AllowUserToOrderColumns = false; _gridView.AllowUserToResizeColumns = false; _gridView.AllowUserToResizeRows = false; _gridView.BorderStyle = System.Windows.Forms.BorderStyle.None; _gridView.CellBorderStyle = System.Windows.Forms.DataGridViewCellBorderStyle.None; _gridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; _gridView.GridColor = System.Drawing.SystemColors.Control; _gridView.MultiSelect = false; _gridView.ReadOnly = true; _gridView.RowHeadersVisible = false; _gridView.RowHeadersWidthSizeMode = System.Windows.Forms.DataGridViewRowHeadersWidthSizeMode.DisableResizing; _gridView.RowTemplate.Height = 24; _gridView.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect; _gridView.TabIndex = 1; _gridView.Dock = DockStyle.Fill; _gridView.BorderStyle = BorderStyle.Fixed3D; _gridView.CellClick += new _gridView.KeyDown += new
{
column.HeaderText = item.Header; column.DataPropertyName = item.DisplayMember;
{ hasCustomColumnSize = true; column.Width = item.Width; } _gridView.Columns.Add(column); }
_gridView.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells; _gridView.DataSource = _bindingSource; _gridView.Font = (Font)editControl.Font.Clone(); clientForm.Controls.Add(_gridView); clientForm.ActiveControl = _gridView; }
{
CloseClientForm(false);
CloseClientForm(true); }
{ CloseClientForm(false); }
{
{
{
_bindingSource.Position = index;
_bindingSource.Position = 0; }
{ } } }
{
{
{
{
}
} }
}
{
{ _gridView.CellClick -= new _gridView.KeyDown -= new _gridView.DataSource = null;
_bindingSource.Dispose(); } } }
[ToolboxItem(true)]
{ [AttributeProvider(typeof(IListSource))] [Category("Data")]
{
{
}
{ ((GridEmbedEditControl)EmbedEditControl).DataSource = value; } }
[TypeConverter(typeof(DataMemberConverter))] [Category("Data")]
{
{
}
{ ((GridEmbedEditControl)EmbedEditControl).DataMember = value; } }
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] [Category("Data")]
{
{
} }
: base() { EmbedEditControl = new } } } |
程式7是這兩個元件所用到的Design-Time程式碼列表。
程式7
using System; using System.ComponentModel; using System.ComponentModel.Design.Serialization; using System.Collections.Generic; using System.Reflection; using System.Text; using System.Globalization; using System.Windows.Forms;
namespace LookupComboBox {
{
{
{
}
{
} }
{
{
}
{
{
} }
} }
{
{
{
}
{
} }
{
{
}
{
}
} }
{
{
list.Add(pd.Name);
}
{
} }
{
{
{
list.Add(pdItem.Name);
}
}
{
} } } |
It's Flexable?
無疑的,OrpEmbedControlEdit及OrpEmbedEditControl的搭配,將這種控件的延展性發揮到一個極致,當然!如果你問我,還有可以增進的空間嗎?我的答案會是有,只是目前尚未想到罷了。
Conclusion
在這兩篇文章中,我跳過了許多的基礎知識,不談Design-Time部份的處理,將重點放在了設計與問題的解決上,這使得這兩篇文章的易讀性降低不少,不過換來的是,你得到了兩個可以立即運用在現實專案上的控件。