Я создал пользовательский элемент управления (названный BoostrapDropDown), который по существу оборачивает кучу надписей Boostrap вокруг DropDownList asp.net.Результирующая иерархия элементов управления будет выглядеть в основном следующим образом со всем, что является HtmlGenericControl, за исключением DropDownList:
<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>
Я «передавал» свойство DataSource из моего элемента управления во вложенный DropDownList, но после обратной передачи ятерял все мои ценности.
Вот неловкая часть.Месяц назад я искал в Интернете и смог создать решение, но я плохо документировал его.И теперь я не могу найти страницы, которые я использовал для создания решения.Я понятия не имею, как это работает, и я надеюсь, что кто-то может пролить свет.Ниже приведен соответствующий исходный код.
ОБНОВЛЕНИЕ : Полный код
// Preventing the EventValidation for dropdown lists b/c they could be populated *only* on the client side;
// https://stackoverflow.com/a/8581311/166231
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:
// https://stackoverflow.com/questions/56299350/saving-viewstate-in-nested-dropdownlist-in-a-custom-control
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
будет толькотам на оригинальном рендеринге.Поэтому я вызываю DataBind
для вложенного DropDownList, чтобы он заполнялся в первый раз, а затем сохраняю все элементы управления обратно в свойство Items
. - Я почти уверен, что понимаю, как
Items
сохраняется / загружается из ViewState. - Где я теряюсь, как свойство my
Items
затем используется для повторного заполнения DropDownList?Я подумал, что, возможно, именно тот факт, что я добавил Load\SaveViewState
(который называется base.Load\SaveViewState
), был тем, что действительно решило мою проблему, но когда я закомментировал все ссылки на мое свойство Items
, я потерял выпадающий списокзначения снова.
Как в мире Items
повторное заполнение inputControl.Items
при обратной передаче?!