using System;
using System.ComponentModel;
using System.Web;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Collections;
using System.Web.UI.Design;
using System.Drawing;
using System.Text;
namespace AtomNet.Web.UI.WebControls
{
[ToolboxData("<{0}:EditableDropDown runat=\"server\"></{0}:EditableDropDown>")]
[Designer(typeof(AtomNet.Web.UI.WebControls.EditableDropDownDesigner))]
public class EditableDropDown : System.Web.UI.WebControls.ListControl
{
/// <summary>
/// 下拉箭头的宽度
/// </summary>
const int DROPDWON_ARROW_WIDTH = 22;
/// <summary>
/// 默认宽度
/// </summary>
const int DEFAULT_WIDTH = 170;
#region 私有属性
/// <summary>
/// 输入框
/// </summary>
TextBox txt = new TextBox();
/// <summary>
/// The path to an external .js file that contains all of the javascript functions
/// needed by this control. If this is not set (the default behavior), then the
/// javascript is emitted inline.
/// </summary>
string externalJsPath = null;
/// <summary>
/// 控件样式
/// </summary>
string externalCssPath = null;
#endregion
#region EditableDropDown构造函数
/// <summary>
/// Initializes a new instance of the ComboBox control
/// </summary>
public EditableDropDown()
{
this.Width = new Unit(DEFAULT_WIDTH);
//计算文本框的宽度
txt.Width = new Unit(this.Width.Value - DROPDWON_ARROW_WIDTH, this.Width.Type);
//此处不要定义该事件,否则与页面js处理keydown,keyup事件冲突,无法正常显示自定义的筛选
//txt.TextChanged += new EventHandler(txt_TextChanged);
}
/*
void txt_TextChanged(object sender, EventArgs e)
{
base.OnTextChanged(e);
}
*/
#endregion
#region 重写 OnInit
/// <summary>
/// Initializes the ComboBox control
/// </summary>
/// <param name="e">The <see cref="EventArgs"/> passed to this method</param>
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
this.ID = string.Format("{0}_dropdownlist", this.ID);
//子控件Render的时候会自动加上父控件的ID前缀_
this.txt.ID = string.Format("{0}_input", this.ClientID);
this.Controls.Add(txt);
}
#endregion
#region 重写OnPreRender
/// <summary>
/// Raises the PreRender event.
/// </summary>
/// <remarks>
/// The common javascript functions must be dealt with here because 'RegisterClientScriptBlock'
/// cannot be called from the Render method (it is too late in the lifesycle).
/// </remarks>
/// <param name="e">An <see cref="EventArgs"/> object that contains the event data.</param>
protected override void OnPreRender(EventArgs e)
{
if (this.Visible)
{
//添加样式
if (this.externalCssPath != null)
{
this.Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "editabledropdown_external_css", String.Format("<link rel='stylesheet' type='text/css' href='{0}' />", this.ResolveUrl(this.externalCssPath)));
}
else
{
if (!this.Page.ClientScript.IsClientScriptBlockRegistered("editabledropdown_external_css"))
{
this.Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "editabledropdown_external_css", CONTROL_CSS);
}
}
// if the javascript is in an external file, render a link to it.
// otherwise, emit the javascript inline.
if (this.externalJsPath != null)
{
this.Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "editabledropdown_external_js", String.Format("<script language='javascript' type='text/javascript' src='{0}'></script>", this.ResolveUrl(this.externalJsPath)));
}
else
{
// however, if another control has already linked in the external file, we
// cant emit the inline javascript because it will conflict. if that is the case,
// we dont have to do anything because it is already all handled.
if (!this.Page.ClientScript.IsClientScriptBlockRegistered("editabledropdown_external_js"))
{
// register the common javascript needed by all instances of the ComboBox control
this.Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "editabledropdown_external_js", CONTROL_JS);
}
}
}
else
{
base.OnPreRender(e);
}
}
#endregion
#region 重写Render
/// <summary>
/// Renders the control as HTML.
/// </summary>
/// <remarks>
/// If the current browser does not support the functionality required to render the
/// ComboBox, a normal <see cref="ListBox"/> will be rendered instead.
/// </remarks>
/// <param name="writer">The <see cref="HtmlTextWriter"/> to which to emit the resulting HTML.</param>
protected override void Render(HtmlTextWriter writer)
{
if (this.Visible)
{
txt.CssClass = "edd_input";
this.CssClass = "edd_dropdownlist";
txt.Attributes.Add("autocomplete","off");
txt.Attributes.Add("onkeydown", "return edd_input_keydown(this,event);");
txt.Attributes.Add("onkeyup", "return edd_input_keyup(this,event);");
txt.Attributes.Add("onfocus", "edd_input_focus(this);");
txt.Attributes.Add("onblur", "edd_input_blur(this);");
this.Attributes.Add("onchange", "edd_dropdownlist_change(this,event);");
// render the control
writer.Write("<div style=\"position:relative;\">");
txt.RenderControl(writer);
//当前总宽度减去padding-left
writer.Write(string.Format("<ul id=\"{0}_droplist\" class=\"edd_droplist\" style=\"width:{1}px\" οnmοuseenter=\"edd_droplist_mouseenter(this)\" οnmοuseleave=\"edd_droplist_mouseleave(this)\"></ul>",
this.ClientID,
this.Width.Value-10));
base.Render(writer);
writer.Write("</div>");
}
else
{
// if the ComboBox is not supported by the current browser, just render
// a plain old ListBox. we also do this if the control is in design-mode
base.Render(writer);
}
}
#endregion
#region 放在工具箱上的属性
/// <summary>
/// Gets or sets the maximum number of characters allowed to be manually entered.
/// </summary>
/// <value>
/// The maxumum number of characters allowed to be manually entered.
/// The default is 0, which indicates that the property is not set.
/// </value>
/// <remarks>
/// If the current browser does not support rendering of the ComboBox, this
/// property has no effect.
/// </remarks>
[Description("Gets or sets the maximum number of characters allowed to be manually entered."),
Browsable(true),
Category("Behavior")]
public int MaxLength
{
get
{
return txt.MaxLength;
}
set
{
txt.MaxLength = value;
}
}
/// <summary>
/// Sets the path to an external .js file that contains all of the javascript
/// required by this control. If not set, the javascript will be emitted inline.
/// </summary>
/// <value>
/// The valid path to the .js file, or <c>null</c>.
/// </value>
/// <remarks>
/// If the current browser does not support rendering of the ComboBox, this
/// property has no effect.
/// </remarks>
[Description("Sets the path to an external .js file that contains all of the javascript required by this control. If not set, the javascript will be emitted inline."),
Browsable(true),
Category("Behavior")]
public string ExternalJsPath
{
get
{
return this.externalJsPath;
}
set
{
this.externalJsPath = value;
}
}
/// <summary>
/// 样式文件路径
/// </summary>
/// <value>
/// The valid path to the .css file, or <c>null</c>.
/// </value>
[Description("Sets the path to an external .js file that contains all of the javascript required by this control. If not set, the javascript will be emitted inline."),
Browsable(true),
Category("Apperance")]
public string ExternalCssPath
{
get
{
return this.externalCssPath;
}
set
{
this.externalCssPath = value;
}
}
/// <summary>
/// 文本
/// </summary>
[Description("文本"),
Browsable(true),
Category("Data")]
public override string Text
{
get
{
return txt.Text;
}
set
{
txt.Text = value;
}
}
/// <summary>
/// 宽度
/// </summary>
[Description("控件宽度"),
Browsable(true),
Category("Apperance"),
DefaultValue("170px")]
public override Unit Width
{
get
{
return base.Width;
}
set
{
base.Width = value;
txt.Width = new Unit(base.Width.Value - DROPDWON_ARROW_WIDTH, base.Width.Type);
}
}
/// <summary>
/// 获取或设置一个值,该值指示当用户更改列表中的选定内容时是否自动产生向服务器的回发
/// </summary>
[Description("获取或设置一个值,该值指示当用户更改列表中的选定内容时是否自动产生向服务器的回发"),
Browsable(true),
Category("Behavior"),
DefaultValue(false)]
public override bool AutoPostBack
{
get
{
return base.AutoPostBack;
}
set
{
base.AutoPostBack = value;
txt.AutoPostBack = value;
}
}
#endregion
#region 脚本
/// <summary>
/// 默认样式
/// </summary>
private const string CONTROL_CSS = @"
<style type='text/css'>
.edd_input{
border: none;
left: 2px;
line-height: 17px;
position: absolute;
top: 1px;
z-index: 1;
}
.edd_droplist
{
border:solid 1px #ccc;
padding-left:10px;
background-color:white;
line-height: 17px;
position: absolute;
z-index: 1;
top:5px;
list-style:none;
display:none;
}
.edd_droplist li
{
cursor:default;
}
.edd_droplist li.active
{
font-weight:bold;
text-decoration:underline
}
.edd_droplist li.selected
{
font-weight:bold;
}
.edd_droplist li:hover
{
text-decoration:underline;
}
</style>
";
/// <summary>
/// The basic javascript that needs to be emitted once per page that contains any ComboBox
/// controls.
/// (This would be nice if it were not in the compiled code so that it could be edited/
/// updated with out a recompile. However, in the interest of making this control self-
/// contained and simple for the demo, it is just included here. It could easily be moved
/// to a seperate .js file that is referenced here.)
/// </summary>
private const string CONTROL_JS = @"
<script type='text/javascript'>
<!--
function edd_droplist_item_click(o)
{
var list = o.parentNode,cid = list.id.replace('_droplist','');
if(list.hasAttribute('activeIndex'))
{
var activeIndex = parseInt(list.getAttribute('activeIndex'));
if(activeIndex > -1)
{
list.childNodes[activeIndex].removeAttribute('class');
}
}
var tag = parseInt(o.getAttribute('tag')),idx = parseInt(o.getAttribute('index'));
document.getElementById(cid).options[tag].selected = true;
o.className = 'selected';
document.getElementById(cid+'_input').value = o.innerHTML;
list.setAttribute('activeIndex',idx);
list.style.display = 'none';
}
function edd_input_keydown(o,e)
{
e = e || window.event;
var k = e.keyCode || e.which;
var cid = o.id.replace('_input',''),
list = document.getElementById(cid+'_droplist');
var activeIndex = list.hasAttribute('activeIndex') ? parseInt(list.getAttribute('activeIndex')):-1;
var total = list.hasAttribute('childCount') ? parseInt(list.getAttribute('childCount')):0;
if(k == 38)
{
if(total > 0)
{
if(activeIndex == -1)
{
activeIndex = total - 1;
}
else if(activeIndex == 0)
{
list.childNodes[activeIndex].removeAttribute('class');
activeIndex = total - 1;
}
else
{
list.childNodes[activeIndex].removeAttribute('class');
activeIndex--;
}
list.childNodes[activeIndex].className = 'active';
list.setAttribute('activeIndex',activeIndex);
if(list.style.display == 'none')
{
edd_show_droplist(list);
}
edd_autoscroll(list,list.childNodes[activeIndex]);
}
o.setAttribute('cancelFilter',1);
return false;
}
else if(k == 40)
{
if(total > 0)
{
if(activeIndex == -1)
{
activeIndex = 0;
}
else if(activeIndex == total - 1)
{
list.childNodes[activeIndex].removeAttribute('class');
activeIndex = 0;
}
else
{
list.childNodes[activeIndex].removeAttribute('class');
activeIndex++;
}
list.childNodes[activeIndex].className = 'active';
list.setAttribute('activeIndex',activeIndex);
if(list.style.display == 'none')
{
edd_show_droplist(list);
}
edd_autoscroll(list,list.childNodes[activeIndex]);
}
o.setAttribute('cancelFilter',1);
return false;
}
else if(k == 13)
{
if(list.childNodes[activeIndex])
{
o.value = list.childNodes[activeIndex].innerHTML;
document.getElementById(cid).options[parseInt(list.childNodes[activeIndex].getAttribute('tag'))].selected = true;
}
list.style.display = 'none';
o.setAttribute('cancelFilter',1);
return false;
}
return true;
}
function edd_autoscroll(l,i)
{
if(i.offsetTop < l.scrollTop
|| l.scrollTop + l.offsetHeight < i.offsetTop)
{
l.scrollTop = i.offsetTop;
}
}
function edd_input_keyup(o,e)
{
if(o.hasAttribute('cancelFilter'))
{
o.removeAttribute('cancelFilter');
return false;
}
var cid = o.id.replace('_input',''),
list = document.getElementById(cid+'_droplist');
if(list)
{
list.style.display = 'none';
list.innerHTML = '';
list.removeAttribute('activeIndex');
list.removeAttribute('childCount');
var str = o.value;
if(str && str.length > 0)
{
var ops = [];
var tstr;
var dropdown = document.getElementById(cid);
if(dropdown && dropdown.options && dropdown.options.length > 0)
{
for(var i =0;i<dropdown.options.length;i++)
{
if(dropdown.options[i].text.indexOf(str) > -1)
{
tstr = dropdown.options[i].text;
ops.push('<li tag=\''+i+'\' index=\''+ops.length+'\' οnclick=\'edd_droplist_item_click(this);\'>'+tstr+'</li>');
}
}
if(ops.length > 0)
{
list.innerHTML = ops.join('');
edd_show_droplist(list);
list.setAttribute('activeIndex',-1);
list.setAttribute('childCount',ops.length);
}
}
}
}
}
function edd_dropdownlist_change(o,e)
{
var cid = o.id,txt = document.getElementById(cid+'_input');
txt.value = o.options[o.selectedIndex].text;
var list = document.getElementById(cid+'_droplist');
list.innerHTML = '';
list.removeAttribute('activeIndex');
list.removeAttribute('childCount');
}
function edd_droplist_mouseenter(o)
{
o.setAttribute('isActive','1');
}
function edd_droplist_mouseleave(o)
{
o.setAttribute('isActive','0');
window.setTimeout(edd_close_droplist(o.id.replace('_droplist','')),500);
}
function edd_input_focus(o)
{
if(o.hasAttribute('timerID'))
{
window.clearTimeout(parseInt(o.getAttribute('timerID')));
o.removeAttribute('timerID');
}
}
function edd_input_blur(o)
{
if(o.hasAttribute('timerID'))
{
window.clearTimeout(parseInt(o.getAttribute('timerID')));
o.removeAttribute('timerID');
}
var cid = o.id.replace('_input',''),list = document.getElementById(cid+'_droplist');
if(list.style.display != 'none'
&& (!list.hasAttribute('isActive')
|| list.getAttribute('isActive') == '0'))
{
o.setAttribute('timerID',window.setTimeout(edd_close_droplist(cid),500));
}
}
function edd_show_droplist(o)
{
o.style.overflowY = 'auto';
o.style.height = 'auto';
o.style.display = 'inline';
if(o.offsetHeight > 200)
{
o.style.overflowY = 'scroll';
o.style.height = '200px';
}
}
function edd_close_droplist(cid)
{
return function(){
var droplist = document.getElementById(cid+'_droplist'),
txt = document.getElementById(cid+'_input');
txt.removeAttribute('timerID');
if(document.activeElement != txt
&& document.activeElement != droplist)
{
droplist.style.display = 'none';
}
};
}
//-->
</script>
";
#endregion
}
public class EditableDropDownDesigner : ControlDesigner, IDataSourceProvider
{
public EditableDropDownDesigner()
{
//
// TODO: Add constructor logic here
//
}
#region ControlDesigner
public override string GetDesignTimeHtml()
{
EditableDropDown component = (EditableDropDown)base.Component;
StringBuilder html = new StringBuilder("<select style='width:" + component.Width + "'></select>");
return html.ToString();
}
#endregion
#region Proxies of the properties that are involved in DataBinding
/// <summary>
/// This is a proxy for the DataMember field that is required to attach the DataMemberConverter to.
/// </summary>
public string DataMember
{
get
{
return ((EditableDropDown)base.Component).DataMember;
}
set
{
((EditableDropDown)base.Component).DataMember = value;
}
}
/// <summary>
/// This is a proxy for the DataTextField field that is required to attach the DataFieldConverter to.
/// </summary>
public string DataTextField
{
get
{
return ((EditableDropDown)base.Component).DataTextField;
}
set
{
((EditableDropDown)base.Component).DataTextField = value;
}
}
/// <summary>
/// This is a proxy for the DataValueField field that is required to attach the DataFieldConverter to.
/// </summary>
public string DataValueField
{
get
{
return ((EditableDropDown)base.Component).DataValueField;
}
set
{
((EditableDropDown)base.Component).DataValueField = value;
}
}
/// <summary>
/// This is a proxy for the DataSource field that is required to attach the DataSourceConverter to.
/// This is especially required as it allows us to represent the DataSource property as a string
/// rather then as an object.
/// </summary>
public string DataSource
{
get
{
DataBinding binding = DataBindings["DataSource"];
if (binding != null)
return binding.Expression;
return string.Empty;
}
set
{
if ((value == null) || (value.Length == 0))
base.DataBindings.Remove("DataSource");
else
{
DataBinding binding = DataBindings["DataSource"];
if (binding == null)
binding = new DataBinding("DataSource",
typeof(IEnumerable), value);
else
binding.Expression = value;
DataBindings.Add(binding);
}
OnBindingsCollectionChanged("DataSource");
}
}
#endregion
#region Overrides
/// <summary>
/// Set to false so that the control can't be resized on the form.
/// </summary>
public override bool AllowResize
{
get
{
return false;
}
}
/// <summary>
/// Used to modify the Attributes of the 'Data' related fields such that
/// the correct TypeConverters are added to the Attributes. For some reason
/// adding the attributes directly doesn't work.
/// </summary>
/// <param name="properties">The dictionary</param>
protected override void PreFilterProperties(IDictionary properties)
{
base.PreFilterProperties(properties);
PropertyDescriptor prop = (PropertyDescriptor)properties["DataSource"];
if (prop != null)
{
System.ComponentModel.AttributeCollection runtimeAttributes = prop.Attributes;
// make a copy of the original attributes but make room for one extra attribute ie the TypeConverter attribute
Attribute[] attrs = new Attribute[runtimeAttributes.Count + 1];
runtimeAttributes.CopyTo(attrs, 0);
attrs[runtimeAttributes.Count] = new TypeConverterAttribute(typeof(DataSourceConverter));
prop = TypeDescriptor.CreateProperty(this.GetType(), "DataSource", typeof(string), attrs);
properties["DataSource"] = prop;
}
prop = (PropertyDescriptor)properties["DataMember"];
if (prop != null)
{
System.ComponentModel.AttributeCollection runtimeAttributes = prop.Attributes;
Attribute[] attrs = new Attribute[runtimeAttributes.Count + 1];
// make a copy of the original attributes but make room for one extra attribute ie the TypeConverter attribute
runtimeAttributes.CopyTo(attrs, 0);
attrs[runtimeAttributes.Count] = new TypeConverterAttribute(typeof(DataMemberConverter));
prop = TypeDescriptor.CreateProperty(this.GetType(), "DataMember", typeof(string), attrs);
properties["DataMember"] = prop;
}
prop = (PropertyDescriptor)properties["DataValueField"];
if (prop != null)
{
System.ComponentModel.AttributeCollection runtimeAttributes = prop.Attributes;
Attribute[] attrs = new Attribute[runtimeAttributes.Count + 1];
// make a copy of the original attributes but make room for one extra attribute ie the TypeConverter attribute
runtimeAttributes.CopyTo(attrs, 0);
attrs[runtimeAttributes.Count] = new TypeConverterAttribute(typeof(DataFieldConverter));
prop = TypeDescriptor.CreateProperty(this.GetType(), "DataValueField", typeof(string), attrs);
properties["DataValueField"] = prop;
}
prop = (PropertyDescriptor)properties["DataTextField"];
if (prop != null)
{
System.ComponentModel.AttributeCollection runtimeAttributes = prop.Attributes;
Attribute[] attrs = new Attribute[runtimeAttributes.Count + 1];
// make a copy of the original attributes but make room for one extra attribute ie the TypeConverter attribute
runtimeAttributes.CopyTo(attrs, 0);
attrs[runtimeAttributes.Count] = new TypeConverterAttribute(typeof(DataFieldConverter));
prop = TypeDescriptor.CreateProperty(this.GetType(), "DataTextField", typeof(string), attrs);
properties["DataTextField"] = prop;
}
}
#endregion
#region IDataSourceProvider methods
/// <summary>
/// Used by the DataFieldConverter to resolve the DataSource and DataMember combination
/// so that it can populate a dropdown with a list of available fields.
/// </summary>
IEnumerable IDataSourceProvider.GetResolvedSelectedDataSource()
{
DataBinding binding;
binding = this.DataBindings["DataSource"];
if (binding != null)
return DesignTimeData.GetSelectedDataSource(this.Component, binding.Expression, this.DataMember);
return null;
}
/// <summary>
/// Used by the DataMemberConverter to resolve the DataSource which it can then use
/// to populate a drop down box containing a list of available tables.
/// </summary>
/// <returns>The object that is our DataSource</returns>
object IDataSourceProvider.GetSelectedDataSource()
{
DataBinding binding;
binding = this.DataBindings["DataSource"];
if (binding != null)
return DesignTimeData.GetSelectedDataSource(this.Component, binding.Expression);
return null;
}
#endregion
}
}
参考AtomNet ComboBox,所以命名空间没有改;
使用方法:
<%@ Register Assembly="CommonControls" Namespace="AtomNet.Web.UI.WebControls" TagPrefix="AtomNet" %>