読者です 読者をやめる 読者になる 読者になる

個人的なメモ 〜Cocos Sharp 情報を中心に‥

Tomohiro Suzuki @hiro128_777 のブログです。Cocos Sharp の事を中心に書いています。 Microsoft MVP for Visual Studio and Development Technologies 2017- 本ブログと所属組織の公式見解は関係ございません。

ASP.NET Web Forms で DropDownList を ReadOnly にしたい時

ASP.NET Web Forms でDropDownListのitemの選択状態を固定したい状況になったとき、JavaScript で disabled にしてしまうとブラウザ上で見たときにグレーに表示されて見難くなったりします。

そこで、HTMLレンダリング前に SelectedValue プロパティで指定された項目以外を強制的に削除してしまい、item を一つだけにしてしまうことで ReadOnly を表現する DropDownList を作りました。

このやり方だと、ユーザが DropDownList をクリックしても選択肢がそれ一つしかないので選択状態を変更できませんし、別途 JavaScript 等必要ないので ASP.NET のみで完結できます。

ポイントは Items のコレクションの中から SelectedItem をコピーした後、Items のコレクションを全削除し、改めて先ほどコピーした、SelectedItem を追加するのですが、そのときに、明示的にディープコピーしてあげないと、単なる参照の保持なのでコピーしておいた SelectedItem の参照先がItems のコレクションを全削除したときに削除されてしまうということです。

オブジェクトのインスタンスをディープコピーをするのに最もお手軽なのはシリアライズしてからデシリアライズすることですが、それが使えない場合は、値をすべて文字列に変換した上で、String.Copy()します。String.Copy()は新しいインスタンスを作成する、つまりディープコピーするメソッドです。

以下コードです。

using System;
using System.ComponentModel;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace Application.Web.UI.WebControls
{
	/// <summary>
	/// ReadOnly 属性をサポートする
	/// ユーザーがドロップダウン リストから単一の項目を選択できるコントロールを表します。 
	/// </summary>
	/// <remarks>
	/// </remarks>
	[ToolboxData("<{0}:ExtendedDropDownList runat=server></{0}:ExtendedDropDownList>")]
	public class ExtendedDropDownList : System.Web.UI.WebControls.DropDownList
	{
		/// <summary>
		/// リスト コントロール内の項目を選択されている項目のみ表示するかどうかを示す値を取得または設定します。
		/// </summary>
		[Bindable(true, BindingDirection.TwoWay)]
		[Browsable(true)]
		[Category("Extended")]
		[DefaultValue(false)]
		[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
		[EditorBrowsableAttribute(EditorBrowsableState.Always)]
		[Localizable(false)]
		[PersistenceMode(PersistenceMode.Attribute)]
		[Themeable(false)]
		public virtual bool ReadOnly
		{
			get
			{
				object obj = this.ViewState["ReadOnly"];
				if(obj != null)
				{
					return (bool)obj;
				}
				return false;
			}
			set
			{
				if(value != this.ReadOnly)
				{
					this.ViewState["ReadOnly"] = value;
				}
				this.EnsureItems();
			}
		}

		/// <summary>
		/// ポストされたデータをロードする。クライアントで動的にドロップダウン項目が作成された
		/// 場合に対応できるように、ポストされたデータがオリジナルのデータにない場合は、
		/// ドロップダウン項目として追加するかどうかを示す値を取得または設定します。
		/// </summary>
		[Bindable(true, BindingDirection.TwoWay)]
		[Browsable(true)]
		[Category("Extended")]
		[DefaultValue(false)]
		[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
		[EditorBrowsableAttribute(EditorBrowsableState.Always)]
		[Localizable(false)]
		[PersistenceMode(PersistenceMode.Attribute)]
		[Themeable(false)]
		public virtual bool EnabledCascade
		{
			get
			{
				object obj = this.ViewState["EnabledCascade"];
				if (obj != null)
				{
					return (bool)obj;
				}
				return false;
			}
			set
			{
				if (value != this.EnabledCascade)
				{
					this.ViewState["EnabledCascade"] = value;
				}
			}
		}

		public override string SelectedValue
		{
			get
			{
				return base.SelectedValue;
			}
			set
			{
				// 選択項目に存在するときのみSetする。
				if (this.Contains(value) == true)
				{
					base.SelectedValue = value;
				}
			}
		}

		/// <summary>
		/// PreRender イベントを発生させます。 
		/// </summary>
		/// <param name="e">イベント データを格納している EventArgs オブジェクト。</param>
		protected override void OnPreRender(EventArgs e)
		{
			this.EnsureItems();
			base.OnPreRender(e);
		}

		/// <summary>
		/// 読み取り専用のとき選択状態の項目以外を削除します。
		/// </summary>
		protected virtual void EnsureItems()
		{
			if (this.ReadOnly == true)
			{
				ListItem selectedItem = new ListItem();
				foreach (ListItem item in this.Items)
				{
					if(item.Value == this.SelectedValue)
					{
						item.Selected = true;
						// ディープコピーします。
						bool enabled;
						bool.TryParse(((bool)item.Enabled).ToString(), out enabled);
						bool selected;
						bool.TryParse(((bool)item.Selected).ToString(), out selected);
						string text = String.Copy(item.Text);
						string value = String.Copy(item.Value);
						selectedItem.Enabled = enabled;
						selectedItem.Selected = true;
						selectedItem.Text = text;
						selectedItem.Value = value;
						// ※SelectedIndexはソースを参照するとSelected = trueなitemのindexを返している。
						break;
					}
				}
				this.Items.Clear();
				this.Items.Add(selectedItem);
				// ClearChildViewState メソッドの不必要な呼び出しをしないようにします。
				if(HasChildViewState)
				{
					ClearChildViewState();
				}
				ChildControlsCreated = true;
				if(!IsTrackingViewState)
				{
					TrackViewState();
				}
				if(String.IsNullOrEmpty(selectedItem.Value) == true)
				{
					return;
				}
				this.SelectedValue = selectedItem.Value;
				this.SelectedItem.Enabled = selectedItem.Enabled;
				this.SelectedItem.Selected = selectedItem.Selected;
				this.SelectedItem.Text = selectedItem.Text;
				this.SelectedItem.Value = selectedItem.Value;
			}
		}

		/// <summary>
		/// ポストされたデータをロードする。クライアントで動的にドロップダウン項目が作成された
		/// 場合に対応できるように、ポストされたデータがオリジナルのデータにない場合は、
		/// ドロップダウン項目として追加する。
		/// </summary>
		protected override bool LoadPostData(string postDataKey, System.Collections.Specialized.NameValueCollection postCollection)
		{
			if (this.EnabledCascade)
			{
				string value = postCollection[postDataKey];
				if (value != null && this.Items.FindByValue(value) == null)
				{
					Items.Add(value);
					SelectedValue = value;
				}
			}
			return base.LoadPostData(postDataKey, postCollection);
		}

		private bool Contains(string item)
		{
			bool returnValue = false;
			foreach (ListItem listItem in this.Items)
			{
				if (item == listItem.Value)
				{
					return true;
				}
			}
			return returnValue;
		}

	}
}