在自定义控件的嵌套 DropDownList 中保存 ViewState
Saving ViewState in Nested DropDownList in a Custom Control
我创建了一个自定义控件(称为 BoostrapDropDown),它基本上将一堆 boostrap 标记包裹在 asp.net DropDownList 周围。生成的控件层次结构基本上如下所示,除了 DropDownList 之外的所有内容都是 HtmlGenericControl:
<div class="form-group viInputID">
<label for="iInputID" class="control-label liInputID"></label>
<a style="display: none;" class="vhiInputID" role="button" tabindex="0" data-toggle="popover" data-trigger="click" data-content-selector=".hiInputID" data-placement="top">
<span class="glyphicon glyphicon-info-sign help-icon"></span>
</a>
<a style="display: none;" class="vsiInputID" role="button" tabindex="0">
<span class="glyphicon glyphicon-volume-up"></span>
</a>
<div class="validator-container">
<asp:DropDownList CssClass="form-control selectpicker show-tick iInputID" data-size="15" ID="iInputID" runat="server" DataSource='<%# DataSource %>' DataTextField="name" DataValueField="key"/>
<span class="error-msg" data-toggle="tooltip" data-placement="top"></span>
</div>
<div class="hiInputIDTitle" style="display: none;"></div>
<div class="hiInputID" style="display: none;"></div>
</div>
我是 'passing through' 从我的控件到嵌套的 DropDownList 的数据源 属性 但是在回发时,我失去了所有的值。
这是尴尬的部分。一个月前,我在网上搜索并能够创建一个解决方案,但我没有很好地记录它。现在我找不到用于创建解决方案的页面。我不知道它是如何工作的,我希望有人能阐明一些问题。下面是相关的源代码。
更新:完整代码
// Preventing the EventValidation for dropdown lists b/c they could be populated *only* on the client side;
//
public class DynamicDropDownList : DropDownList { }
public class DynamicListBox : ListBox { }
public class HtmlGenericControlWithCss : HtmlGenericControl
{
public HtmlGenericControlWithCss(string tag) : base(tag) { }
public HtmlGenericControlWithCss(string tag, string css) : this(tag)
{
Attributes["class"] = css;
}
public HtmlGenericControlWithCss(string tag, string css, string style) : this(tag, css)
{
Attributes["style"] = style;
}
}
public class HtmlAnchorWithCss : HtmlAnchor
{
public HtmlAnchorWithCss(string css) : base()
{
Attributes["class"] = css;
}
public HtmlAnchorWithCss(string css, string style) : this(css)
{
Attributes["style"] = style;
}
}
public abstract class BootstrapInputBase : WebControl, INamingContainer
{
protected HtmlGenericControl formGroup;
protected bool isBootstrap4;
public string HelpPlacement
{
get => (string)ViewState["HelpPlacement"] ?? "top";
set => ViewState["HelpPlacement"] = value;
}
public string Label
{
get => (string)ViewState[nameof(Label)];
set => ViewState[nameof(Label)] = value;
}
public string LabelCss
{
get => (string)ViewState[nameof(LabelCss)];
set => ViewState[nameof(LabelCss)] = value;
}
public string HelpContent
{
get => (string)ViewState[nameof(HelpContent)];
set => ViewState[nameof(HelpContent)] = value;
}
public override void RenderControl(HtmlTextWriter writer)
{
using (var sw = new StringWriter())
using (var hw = new HtmlTextWriter(sw))
{
base.RenderControl(hw);
// need formatted so browser renders it nice (otherwise wierd spacing issues if some of the whitespace is removed)
var html = XElement.Parse(sw.ToString());
writer.Write(html.ToString());
}
}
public void AddControl(Control control)
{
EnsureChildControls();
formGroup.Controls.Add(control);
}
protected override void CreateChildControls()
{
isBootstrap4 = true;
/*
<div class="form-group viInputID">
<label for="iInputID" class="control-label liInputID"></label>
<a style="display: none;" class="vhiInputID" role="button" tabindex="0" data-toggle="popover" data-trigger="click" data-content-selector=".hiInputID" data-placement="top">
<span class="glyphicon glyphicon-info-sign help-icon"></span>
</a>
<a style="display: none;" class="vsiInputID" role="button" tabindex="0">
<span class="glyphicon glyphicon-volume-up"></span>
</a>
<div class="validator-container"> [abstract] </div>
<div class="hiInputIDTitle" style="display: none;"></div>
<div class="hiInputID" style="display: none;"></div>
</div>
*/
formGroup = new HtmlGenericControlWithCss("div", "form-group v" + ID);
Controls.Add(formGroup);
formGroup.Controls.Add(CreateLabel());
var help = new HtmlAnchorWithCss("vh" + ID, string.IsNullOrEmpty(HelpContent) ? "display: none;" : null);
help.Attributes["role"] = "button";
help.Attributes["tabindex"] = "0";
help.Attributes["data-toggle"] = "popover";
help.Attributes["data-trigger"] = "click";
help.Attributes["data-content-selector"] = ".h" + ID;
help.Attributes["data-placement"] = HelpPlacement;
// Couldn't use server controls b/c it put <a><span .../></a> with no space, if newline before span, then HTML rendered a little break after the label
// help.InnerHtml = Environment.NewLine + "<span class='glyphicon glyphicon-info-sign help-icon'></span>";
formGroup.Controls.Add(help);
help.Controls.Add(new HtmlGenericControlWithCss("span", isBootstrap4 ? "fal fa-question-circle help-icon" : "glyphicon glyphicon-info-sign help-icon"));
var voice = new HtmlAnchorWithCss("vs" + ID, "display: none;");
voice.Attributes["role"] = "button";
voice.Attributes["tabindex"] = "0";
// Couldn't use server controls b/c it put <a><span .../></a> with no space, if newline before span, then HTML rendered a little break after the label
// voice.InnerHtml = Environment.NewLine + "<span class='glyphicon glyphicon-volume-up'></span>";
formGroup.Controls.Add(voice);
voice.Controls.Add(new HtmlGenericControlWithCss("span", isBootstrap4 ? "fal fa-volume-up" : "glyphicon glyphicon-volume-up"));
formGroup.Controls.Add(CreateValidatorContainer());
formGroup.Controls.Add(new HtmlGenericControlWithCss("div", "h" + ID, "display: none;") { InnerHtml = HelpContent });
formGroup.Controls.Add(new HtmlGenericControlWithCss("div", "h" + ID + "Title", "display: none;"));
}
protected abstract HtmlGenericControl CreateValidatorContainer();
public abstract string Value { get; set; }
protected virtual HtmlGenericControl CreateLabel()
{
var label = new HtmlGenericControlWithCss("label", "control-label l" + ID + (!string.IsNullOrEmpty(LabelCss) ? " " + LabelCss : "")) { InnerHtml = Label, EnableViewState = true };
label.Attributes["for"] = ID;
return label;
}
protected virtual HtmlGenericControl CreateErrorMessage()
{
var errorMessage = new HtmlGenericControlWithCss("span", "error-msg");
errorMessage.Attributes["data-toggle"] = "tooltip";
errorMessage.Attributes["data-placement"] = "top auto";
return errorMessage;
}
}
public class BootstrapDropDown : BootstrapInputBase
{
private ListControl inputControl;
// If this is false and the client wants to postback to the server for processing,
// I would need to try to grab values via Request.Form[ UniqueID + ":" + ID ].
// But the CalcEngine would *have* to validate the item is inside a known list and
// no malicious values were posted back to server.
public bool SupportEventValidation
{
get => (bool?)ViewState[nameof(SupportEventValidation)] ?? true;
set => ViewState[nameof(SupportEventValidation)] = value;
}
public bool AllowMultiSelect
{
get => (bool?)ViewState[nameof(AllowMultiSelect)] ?? false;
set => ViewState[nameof(AllowMultiSelect)] = value;
}
public string DataTextField
{
get => (string)ViewState[nameof(DataTextField)];
set => ViewState[nameof(DataTextField)] = value;
}
public string DataValueField
{
get => (string)ViewState[nameof(DataValueField)];
set => ViewState[nameof(DataValueField)] = value;
}
public object DataSource { get; set; }
ListItemCollection items;
public virtual ListItemCollection Items
{
get
{
if (items == null)
{
items = new ListItemCollection();
if (IsTrackingViewState)
{
((IStateManager)items).TrackViewState();
}
}
return items;
}
}
public ListControl ListControl
{
get
{
// Don't want this, would like to just use Items property
// to clear/add items but wasn't working and I still don't understand
// how my dropdown list is retaining view state. SO Question:
//
EnsureChildControls();
return inputControl;
}
}
protected override void LoadViewState(object savedState)
{
var allState = (object[])savedState;
HelpContent = (string)allState[4];
Label = (string)allState[3];
Value = (string)allState[2];
((IStateManager)Items).LoadViewState(allState[1]);
base.LoadViewState(allState[0]);
}
protected override object SaveViewState()
{
var allState = new object[5];
allState[0] = base.SaveViewState();
allState[1] = ((IStateManager)Items).SaveViewState();
allState[2] = Value;
allState[3] = Label;
allState[4] = HelpContent;
return allState;
}
public override string Value
{
get
{
EnsureChildControls();
return inputControl.SelectedValue;
}
set
{
EnsureChildControls();
inputControl.SelectedValue = value;
}
}
public string SelectedValue => Value;
public virtual string Text
{
get
{
EnsureChildControls();
return inputControl.SelectedItem?.Text;
}
}
protected override HtmlGenericControl CreateValidatorContainer()
{
/*
<div class="validator-container">
<asp:DropDownList CssClass="form-control selectpicker show-tick iInputID" data-size="15" ID="iInputID" runat="server" DataSource='<%# xDSHelper.GetDataTable( "TableTaxStatus" ) %>' DataTextField="name" DataValueField="key"/>
<span class="error-msg" data-toggle="tooltip" data-placement="top"></span>
</div>
*/
var validatorContainer = new HtmlGenericControlWithCss("div", "validator-container");
inputControl = SupportEventValidation
? AllowMultiSelect
? new ListBox() { CssClass = "form-control selectpicker show-tick " + ID, ID = ID, DataValueField = DataValueField, DataTextField = DataTextField, DataSource = DataSource, SelectionMode = ListSelectionMode.Multiple } as ListControl
: new DropDownList() { CssClass = "form-control selectpicker show-tick " + ID, ID = ID, DataValueField = DataValueField, DataTextField = DataTextField, DataSource = DataSource } as ListControl
: AllowMultiSelect
? new DynamicListBox() { CssClass = "form-control selectpicker show-tick " + ID, ID = ID, DataValueField = DataValueField, DataTextField = DataTextField, DataSource = DataSource, SelectionMode = ListSelectionMode.Multiple } as ListControl
: new DynamicDropDownList() { CssClass = "form-control selectpicker show-tick " + ID, ID = ID, DataValueField = DataValueField, DataTextField = DataTextField, DataSource = DataSource } as ListControl;
inputControl.Attributes["data-size"] = "15";
if (AllowMultiSelect)
{
inputControl.Attributes["data-selected-text-format"] = "count > 2";
}
else
{
inputControl.Attributes["data-live-search"] = "true";
}
validatorContainer.Controls.Add(inputControl);
if (DataSource != null)
{
inputControl.DataBind();
Items.AddRange(inputControl.Items.Cast<ListItem>().ToArray());
}
validatorContainer.Controls.Add(CreateErrorMessage());
return validatorContainer;
}
}
并且通过以下方式在标记中使用该控件:
<mh:BootstrapDropDown runat="server" ID="iGroup" Label="Select Group Name" EnableViewState="true" DataSource='<%# Groups %>' DataTextField="Text" DataValueField="Value" />
然后在后面的代码中,有以下内容:
protected System.Collections.ArrayList Groups
{
get
{
var al = new System.Collections.ArrayList();
al.Add(new ListItem("[Select a Group]", ""));
al.Add(new ListItem("Group A", "A"));
al.Add(new ListItem("Group B", "B"));
return al;
}
}
所以这是我的困惑...
- 在
CreateChildControls
期间,DataSource
只会出现在原始渲染中。所以我在嵌套的 DropDownList 上调用 DataBind
让它第一次填充,然后我将所有控件 Items 存储回 Items
属性.
- 我很确定我理解
Items
是如何从 ViewState 持久化 to/loaded 的。
- 我迷路了,my
Items
属性 是如何习惯重新填充 DropDownList 的?我在想这可能是我添加 Load\SaveViewState
(称为 base.Load\SaveViewState
)的事实真正解决了我的问题,但是当我注释掉所有对我的 Items
[=56 的引用时=],我又失去了下拉列表的值。
Items
是如何在回发时重新填充 inputControl.Items
的?!
我理解最终问题是:
How in the world is Items repopulating inputControl.Items on
postback?!
不过,我认为这个问题不需要(或不应该)回答,原因有二:
您的初始需求声明:
I created a custom control that essentially
wraps a bunch of boostrap markup around a asp.net DropDownList.
您的代码(我指的是您的代码的原始版本,它对我们的讨论来说足够好且足够长)包含许多与持久化自定义控件属性有关的技术ViewState 中的复杂类型(LoadViewState
、SaveViewState
、Triplet
、IStateManager
等),但其中大部分是 not 在你的情况下需要 因为(此时你的需求声明变得至关重要):
BootstrapDropDown
只是一个复合自定义控件,它嵌入了 DropDownList
并且可以(并且应该)委托 对它进行所有工作!
事实上,您已经很好地完成了 Text
和 Value
属性。为什么不为 Items
属性 也这样做呢?您的控件按 组合 工作。它不需要维护自己的 ListItemCollection
更不用说在 ViewState 中传递它了。
最后但同样重要的是,记住嵌入式服务器控件将自动管理它们自己的 ViewState 非常重要。换句话说,您无需手动管理 inputControl
.
的 ViewState
话虽如此,这里有一个基于您的(原始)代码的示例,无需黑魔法即可运行:
public class BootstrapDropDown : WebControl, INamingContainer
{
private DropDownList inputControl;
public string DataTextField
{
get => (string)ViewState[nameof(DataTextField)];
set => ViewState[nameof(DataTextField)] = value;
}
public string DataValueField
{
get => (string)ViewState[nameof(DataValueField)];
set => ViewState[nameof(DataValueField)] = value;
}
public IEnumerable DataSource { get; set; }
public virtual ListItemCollection Items
{
get
{
EnsureChildControls();
return inputControl.Items;
}
}
public virtual string Value
{
get
{
EnsureChildControls();
return inputControl.SelectedValue;
}
set
{
EnsureChildControls();
inputControl.SelectedValue = value;
}
}
public virtual string Text
{
get
{
EnsureChildControls();
return inputControl.SelectedItem?.Text;
}
}
protected override void CreateChildControls()
{
/* Added other html markup controls described above */
var validatorContainer = new HtmlGenericControl("div");
validatorContainer.Attributes["class"] = "validator-container";
inputControl = new DropDownList() {
CssClass = "form-control selectpicker show-tick " + ID,
ID = ID,
DataValueField = DataValueField,
DataTextField = DataTextField,
DataSource = DataSource
};
inputControl.Attributes["data-size"] = "15";
inputControl.Attributes["data-live-search"] = "true";
validatorContainer.Controls.Add(inputControl);
Controls.Add(validatorContainer);
if (DataSource != null)
{
inputControl.DataBind();
}
/* Added other html markup controls described */
}
}
ASPX:
<mh:BootstrapDropDown
runat="server"
ID="iGroup"
Label="Select Group Name"
DataSource='<%# Groups %>'
DataTextField="Text"
DataValueField="Value" />
<asp:Button ID="Button1" runat="server" Text="Button" OnClick="Button1_Click" /><br />
<asp:Label ID="Label1" runat="server" Text=""></asp:Label><br />
<asp:Label ID="Label2" runat="server" Text=""></asp:Label>
后面的代码:
protected System.Collections.ArrayList Groups
{
get
{
var al = new System.Collections.ArrayList();
al.Add(new ListItem("[Select a Group]", ""));
al.Add(new ListItem("Group A", "A"));
al.Add(new ListItem("Group B", "B"));
return al;
}
}
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
DataBind();
}
}
protected void Button1_Click(object sender, EventArgs e)
{
Label1.Text = iGroup.Text;
Label2.Text = iGroup.Value;
}
最后一件事值得一提。注意 inputControl
在 被添加到 Controls
集合后被数据绑定 。这很重要,因为将控件添加到集合也是控件开始跟踪其 ViewState 的点。您可以在这篇优秀的文章中阅读更多(或全部)相关内容:
https://weblogs.asp.net/infinitiesloop/Truly-Understanding-Viewstate
此外,我在 Dino Esposito 的这篇文章中找到了对 IStateManager
机制的引用:
https://www.itprotoday.com/web-application-management/inside-aspnet-control-properties
我创建了一个自定义控件(称为 BoostrapDropDown),它基本上将一堆 boostrap 标记包裹在 asp.net DropDownList 周围。生成的控件层次结构基本上如下所示,除了 DropDownList 之外的所有内容都是 HtmlGenericControl:
<div class="form-group viInputID">
<label for="iInputID" class="control-label liInputID"></label>
<a style="display: none;" class="vhiInputID" role="button" tabindex="0" data-toggle="popover" data-trigger="click" data-content-selector=".hiInputID" data-placement="top">
<span class="glyphicon glyphicon-info-sign help-icon"></span>
</a>
<a style="display: none;" class="vsiInputID" role="button" tabindex="0">
<span class="glyphicon glyphicon-volume-up"></span>
</a>
<div class="validator-container">
<asp:DropDownList CssClass="form-control selectpicker show-tick iInputID" data-size="15" ID="iInputID" runat="server" DataSource='<%# DataSource %>' DataTextField="name" DataValueField="key"/>
<span class="error-msg" data-toggle="tooltip" data-placement="top"></span>
</div>
<div class="hiInputIDTitle" style="display: none;"></div>
<div class="hiInputID" style="display: none;"></div>
</div>
我是 'passing through' 从我的控件到嵌套的 DropDownList 的数据源 属性 但是在回发时,我失去了所有的值。
这是尴尬的部分。一个月前,我在网上搜索并能够创建一个解决方案,但我没有很好地记录它。现在我找不到用于创建解决方案的页面。我不知道它是如何工作的,我希望有人能阐明一些问题。下面是相关的源代码。
更新:完整代码
// Preventing the EventValidation for dropdown lists b/c they could be populated *only* on the client side;
//
public class DynamicDropDownList : DropDownList { }
public class DynamicListBox : ListBox { }
public class HtmlGenericControlWithCss : HtmlGenericControl
{
public HtmlGenericControlWithCss(string tag) : base(tag) { }
public HtmlGenericControlWithCss(string tag, string css) : this(tag)
{
Attributes["class"] = css;
}
public HtmlGenericControlWithCss(string tag, string css, string style) : this(tag, css)
{
Attributes["style"] = style;
}
}
public class HtmlAnchorWithCss : HtmlAnchor
{
public HtmlAnchorWithCss(string css) : base()
{
Attributes["class"] = css;
}
public HtmlAnchorWithCss(string css, string style) : this(css)
{
Attributes["style"] = style;
}
}
public abstract class BootstrapInputBase : WebControl, INamingContainer
{
protected HtmlGenericControl formGroup;
protected bool isBootstrap4;
public string HelpPlacement
{
get => (string)ViewState["HelpPlacement"] ?? "top";
set => ViewState["HelpPlacement"] = value;
}
public string Label
{
get => (string)ViewState[nameof(Label)];
set => ViewState[nameof(Label)] = value;
}
public string LabelCss
{
get => (string)ViewState[nameof(LabelCss)];
set => ViewState[nameof(LabelCss)] = value;
}
public string HelpContent
{
get => (string)ViewState[nameof(HelpContent)];
set => ViewState[nameof(HelpContent)] = value;
}
public override void RenderControl(HtmlTextWriter writer)
{
using (var sw = new StringWriter())
using (var hw = new HtmlTextWriter(sw))
{
base.RenderControl(hw);
// need formatted so browser renders it nice (otherwise wierd spacing issues if some of the whitespace is removed)
var html = XElement.Parse(sw.ToString());
writer.Write(html.ToString());
}
}
public void AddControl(Control control)
{
EnsureChildControls();
formGroup.Controls.Add(control);
}
protected override void CreateChildControls()
{
isBootstrap4 = true;
/*
<div class="form-group viInputID">
<label for="iInputID" class="control-label liInputID"></label>
<a style="display: none;" class="vhiInputID" role="button" tabindex="0" data-toggle="popover" data-trigger="click" data-content-selector=".hiInputID" data-placement="top">
<span class="glyphicon glyphicon-info-sign help-icon"></span>
</a>
<a style="display: none;" class="vsiInputID" role="button" tabindex="0">
<span class="glyphicon glyphicon-volume-up"></span>
</a>
<div class="validator-container"> [abstract] </div>
<div class="hiInputIDTitle" style="display: none;"></div>
<div class="hiInputID" style="display: none;"></div>
</div>
*/
formGroup = new HtmlGenericControlWithCss("div", "form-group v" + ID);
Controls.Add(formGroup);
formGroup.Controls.Add(CreateLabel());
var help = new HtmlAnchorWithCss("vh" + ID, string.IsNullOrEmpty(HelpContent) ? "display: none;" : null);
help.Attributes["role"] = "button";
help.Attributes["tabindex"] = "0";
help.Attributes["data-toggle"] = "popover";
help.Attributes["data-trigger"] = "click";
help.Attributes["data-content-selector"] = ".h" + ID;
help.Attributes["data-placement"] = HelpPlacement;
// Couldn't use server controls b/c it put <a><span .../></a> with no space, if newline before span, then HTML rendered a little break after the label
// help.InnerHtml = Environment.NewLine + "<span class='glyphicon glyphicon-info-sign help-icon'></span>";
formGroup.Controls.Add(help);
help.Controls.Add(new HtmlGenericControlWithCss("span", isBootstrap4 ? "fal fa-question-circle help-icon" : "glyphicon glyphicon-info-sign help-icon"));
var voice = new HtmlAnchorWithCss("vs" + ID, "display: none;");
voice.Attributes["role"] = "button";
voice.Attributes["tabindex"] = "0";
// Couldn't use server controls b/c it put <a><span .../></a> with no space, if newline before span, then HTML rendered a little break after the label
// voice.InnerHtml = Environment.NewLine + "<span class='glyphicon glyphicon-volume-up'></span>";
formGroup.Controls.Add(voice);
voice.Controls.Add(new HtmlGenericControlWithCss("span", isBootstrap4 ? "fal fa-volume-up" : "glyphicon glyphicon-volume-up"));
formGroup.Controls.Add(CreateValidatorContainer());
formGroup.Controls.Add(new HtmlGenericControlWithCss("div", "h" + ID, "display: none;") { InnerHtml = HelpContent });
formGroup.Controls.Add(new HtmlGenericControlWithCss("div", "h" + ID + "Title", "display: none;"));
}
protected abstract HtmlGenericControl CreateValidatorContainer();
public abstract string Value { get; set; }
protected virtual HtmlGenericControl CreateLabel()
{
var label = new HtmlGenericControlWithCss("label", "control-label l" + ID + (!string.IsNullOrEmpty(LabelCss) ? " " + LabelCss : "")) { InnerHtml = Label, EnableViewState = true };
label.Attributes["for"] = ID;
return label;
}
protected virtual HtmlGenericControl CreateErrorMessage()
{
var errorMessage = new HtmlGenericControlWithCss("span", "error-msg");
errorMessage.Attributes["data-toggle"] = "tooltip";
errorMessage.Attributes["data-placement"] = "top auto";
return errorMessage;
}
}
public class BootstrapDropDown : BootstrapInputBase
{
private ListControl inputControl;
// If this is false and the client wants to postback to the server for processing,
// I would need to try to grab values via Request.Form[ UniqueID + ":" + ID ].
// But the CalcEngine would *have* to validate the item is inside a known list and
// no malicious values were posted back to server.
public bool SupportEventValidation
{
get => (bool?)ViewState[nameof(SupportEventValidation)] ?? true;
set => ViewState[nameof(SupportEventValidation)] = value;
}
public bool AllowMultiSelect
{
get => (bool?)ViewState[nameof(AllowMultiSelect)] ?? false;
set => ViewState[nameof(AllowMultiSelect)] = value;
}
public string DataTextField
{
get => (string)ViewState[nameof(DataTextField)];
set => ViewState[nameof(DataTextField)] = value;
}
public string DataValueField
{
get => (string)ViewState[nameof(DataValueField)];
set => ViewState[nameof(DataValueField)] = value;
}
public object DataSource { get; set; }
ListItemCollection items;
public virtual ListItemCollection Items
{
get
{
if (items == null)
{
items = new ListItemCollection();
if (IsTrackingViewState)
{
((IStateManager)items).TrackViewState();
}
}
return items;
}
}
public ListControl ListControl
{
get
{
// Don't want this, would like to just use Items property
// to clear/add items but wasn't working and I still don't understand
// how my dropdown list is retaining view state. SO Question:
//
EnsureChildControls();
return inputControl;
}
}
protected override void LoadViewState(object savedState)
{
var allState = (object[])savedState;
HelpContent = (string)allState[4];
Label = (string)allState[3];
Value = (string)allState[2];
((IStateManager)Items).LoadViewState(allState[1]);
base.LoadViewState(allState[0]);
}
protected override object SaveViewState()
{
var allState = new object[5];
allState[0] = base.SaveViewState();
allState[1] = ((IStateManager)Items).SaveViewState();
allState[2] = Value;
allState[3] = Label;
allState[4] = HelpContent;
return allState;
}
public override string Value
{
get
{
EnsureChildControls();
return inputControl.SelectedValue;
}
set
{
EnsureChildControls();
inputControl.SelectedValue = value;
}
}
public string SelectedValue => Value;
public virtual string Text
{
get
{
EnsureChildControls();
return inputControl.SelectedItem?.Text;
}
}
protected override HtmlGenericControl CreateValidatorContainer()
{
/*
<div class="validator-container">
<asp:DropDownList CssClass="form-control selectpicker show-tick iInputID" data-size="15" ID="iInputID" runat="server" DataSource='<%# xDSHelper.GetDataTable( "TableTaxStatus" ) %>' DataTextField="name" DataValueField="key"/>
<span class="error-msg" data-toggle="tooltip" data-placement="top"></span>
</div>
*/
var validatorContainer = new HtmlGenericControlWithCss("div", "validator-container");
inputControl = SupportEventValidation
? AllowMultiSelect
? new ListBox() { CssClass = "form-control selectpicker show-tick " + ID, ID = ID, DataValueField = DataValueField, DataTextField = DataTextField, DataSource = DataSource, SelectionMode = ListSelectionMode.Multiple } as ListControl
: new DropDownList() { CssClass = "form-control selectpicker show-tick " + ID, ID = ID, DataValueField = DataValueField, DataTextField = DataTextField, DataSource = DataSource } as ListControl
: AllowMultiSelect
? new DynamicListBox() { CssClass = "form-control selectpicker show-tick " + ID, ID = ID, DataValueField = DataValueField, DataTextField = DataTextField, DataSource = DataSource, SelectionMode = ListSelectionMode.Multiple } as ListControl
: new DynamicDropDownList() { CssClass = "form-control selectpicker show-tick " + ID, ID = ID, DataValueField = DataValueField, DataTextField = DataTextField, DataSource = DataSource } as ListControl;
inputControl.Attributes["data-size"] = "15";
if (AllowMultiSelect)
{
inputControl.Attributes["data-selected-text-format"] = "count > 2";
}
else
{
inputControl.Attributes["data-live-search"] = "true";
}
validatorContainer.Controls.Add(inputControl);
if (DataSource != null)
{
inputControl.DataBind();
Items.AddRange(inputControl.Items.Cast<ListItem>().ToArray());
}
validatorContainer.Controls.Add(CreateErrorMessage());
return validatorContainer;
}
}
并且通过以下方式在标记中使用该控件:
<mh:BootstrapDropDown runat="server" ID="iGroup" Label="Select Group Name" EnableViewState="true" DataSource='<%# Groups %>' DataTextField="Text" DataValueField="Value" />
然后在后面的代码中,有以下内容:
protected System.Collections.ArrayList Groups
{
get
{
var al = new System.Collections.ArrayList();
al.Add(new ListItem("[Select a Group]", ""));
al.Add(new ListItem("Group A", "A"));
al.Add(new ListItem("Group B", "B"));
return al;
}
}
所以这是我的困惑...
- 在
CreateChildControls
期间,DataSource
只会出现在原始渲染中。所以我在嵌套的 DropDownList 上调用DataBind
让它第一次填充,然后我将所有控件 Items 存储回Items
属性. - 我很确定我理解
Items
是如何从 ViewState 持久化 to/loaded 的。 - 我迷路了,my
Items
属性 是如何习惯重新填充 DropDownList 的?我在想这可能是我添加Load\SaveViewState
(称为base.Load\SaveViewState
)的事实真正解决了我的问题,但是当我注释掉所有对我的Items
[=56 的引用时=],我又失去了下拉列表的值。
Items
是如何在回发时重新填充 inputControl.Items
的?!
我理解最终问题是:
How in the world is Items repopulating inputControl.Items on postback?!
不过,我认为这个问题不需要(或不应该)回答,原因有二:
您的初始需求声明:
I created a custom control that essentially wraps a bunch of boostrap markup around a asp.net DropDownList.
您的代码(我指的是您的代码的原始版本,它对我们的讨论来说足够好且足够长)包含许多与持久化自定义控件属性有关的技术ViewState 中的复杂类型(
LoadViewState
、SaveViewState
、Triplet
、IStateManager
等),但其中大部分是 not 在你的情况下需要 因为(此时你的需求声明变得至关重要):BootstrapDropDown
只是一个复合自定义控件,它嵌入了DropDownList
并且可以(并且应该)委托 对它进行所有工作!
事实上,您已经很好地完成了 Text
和 Value
属性。为什么不为 Items
属性 也这样做呢?您的控件按 组合 工作。它不需要维护自己的 ListItemCollection
更不用说在 ViewState 中传递它了。
最后但同样重要的是,记住嵌入式服务器控件将自动管理它们自己的 ViewState 非常重要。换句话说,您无需手动管理 inputControl
.
话虽如此,这里有一个基于您的(原始)代码的示例,无需黑魔法即可运行:
public class BootstrapDropDown : WebControl, INamingContainer
{
private DropDownList inputControl;
public string DataTextField
{
get => (string)ViewState[nameof(DataTextField)];
set => ViewState[nameof(DataTextField)] = value;
}
public string DataValueField
{
get => (string)ViewState[nameof(DataValueField)];
set => ViewState[nameof(DataValueField)] = value;
}
public IEnumerable DataSource { get; set; }
public virtual ListItemCollection Items
{
get
{
EnsureChildControls();
return inputControl.Items;
}
}
public virtual string Value
{
get
{
EnsureChildControls();
return inputControl.SelectedValue;
}
set
{
EnsureChildControls();
inputControl.SelectedValue = value;
}
}
public virtual string Text
{
get
{
EnsureChildControls();
return inputControl.SelectedItem?.Text;
}
}
protected override void CreateChildControls()
{
/* Added other html markup controls described above */
var validatorContainer = new HtmlGenericControl("div");
validatorContainer.Attributes["class"] = "validator-container";
inputControl = new DropDownList() {
CssClass = "form-control selectpicker show-tick " + ID,
ID = ID,
DataValueField = DataValueField,
DataTextField = DataTextField,
DataSource = DataSource
};
inputControl.Attributes["data-size"] = "15";
inputControl.Attributes["data-live-search"] = "true";
validatorContainer.Controls.Add(inputControl);
Controls.Add(validatorContainer);
if (DataSource != null)
{
inputControl.DataBind();
}
/* Added other html markup controls described */
}
}
ASPX:
<mh:BootstrapDropDown
runat="server"
ID="iGroup"
Label="Select Group Name"
DataSource='<%# Groups %>'
DataTextField="Text"
DataValueField="Value" />
<asp:Button ID="Button1" runat="server" Text="Button" OnClick="Button1_Click" /><br />
<asp:Label ID="Label1" runat="server" Text=""></asp:Label><br />
<asp:Label ID="Label2" runat="server" Text=""></asp:Label>
后面的代码:
protected System.Collections.ArrayList Groups
{
get
{
var al = new System.Collections.ArrayList();
al.Add(new ListItem("[Select a Group]", ""));
al.Add(new ListItem("Group A", "A"));
al.Add(new ListItem("Group B", "B"));
return al;
}
}
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
DataBind();
}
}
protected void Button1_Click(object sender, EventArgs e)
{
Label1.Text = iGroup.Text;
Label2.Text = iGroup.Value;
}
最后一件事值得一提。注意 inputControl
在 被添加到 Controls
集合后被数据绑定 。这很重要,因为将控件添加到集合也是控件开始跟踪其 ViewState 的点。您可以在这篇优秀的文章中阅读更多(或全部)相关内容:
https://weblogs.asp.net/infinitiesloop/Truly-Understanding-Viewstate
此外,我在 Dino Esposito 的这篇文章中找到了对 IStateManager
机制的引用:
https://www.itprotoday.com/web-application-management/inside-aspnet-control-properties