c# - 在自定义控件的嵌套 DropDownList 中保存 ViewState

标签 c# asp.net custom-controls viewstate composite-controls

我创建了一个自定义控件(称为 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>

我正在将一个 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;
    }
}

所以这是我的困惑......

  1. CreateChildControls 期间,DataSource 只会出现在原始渲染中。因此,我在嵌套的 DropDownList 上调用 DataBind 以使其第一次填充,然后我将所有控件 Items 存储回 Items 属性。
  2. 我很确定我了解 Items 是如何保存到 ViewState 或从 ViewState 加载的。
  3. 我迷路的地方是,我的 Items 属性是如何被用来重新填充 DropDownList 的?我在想可能是因为我添加了 Load\SaveViewState(称为 base.Load\SaveViewState)才真正解决了我的问题,但是当我注释掉时所有对我的 Items 属性的引用,我再次丢失了下拉列表值。

Items 是如何在回发时重新填充 inputControl.Items 的?!

最佳答案

我明白最终的问题是:

How in the world is Items repopulating inputControl.Items on postback?!

不过,我认为这个问题不需要(或不应该)回答,原因有二:

  1. 您的初始需求声明:

    I created a custom control that essentially wraps a bunch of boostrap markup around a asp.net DropDownList.

  2. 事实上,您的代码(我指的是您代码的原始版本,对于我们的讨论来说足够好且足够长)包含许多技术,这些技术与持久化复杂类型的自定义控件属性有关ViewState(LoadViewStateSaveViewStateTripletIStateManager 等)但其中大部分是 在您的情况下不需要,因为(此时您的需求声明变得至关重要):

    BootstrapDropDown 只是一个复合自定义控件,它嵌入了一个 DropDownList 并且可以(并且应该)委派所有工作给它!

事实上,您已经很好地为 TextValue 属性完成了该操作。为什么不对 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 集合后被数据绑定(bind)。这很重要,因为将控件添加到集合也是控件开始跟踪其 ViewState 的点。您可以在这篇优秀的文章中阅读更多(或全部)相关内容:

https://weblogs.asp.net/infinitiesloop/Truly-Understanding-Viewstate

此外,我在 Dino Esposito 的这篇文章中找到了对 IStateManager 机制的引用:

https://www.itprotoday.com/web-application-management/inside-aspnet-control-properties

关于c# - 在自定义控件的嵌套 DropDownList 中保存 ViewState,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56299350/

相关文章:

c# - WindowsIdentity.GetCurrent().Name 获取应用程序池名称而不是用户名

c# - 有更好的方法来处理用户输入验证吗?

c# - 为 .NET 进程生成带有符号的转储文件

cocoa - 基于 NSView 的自定义控件绘图更新被延迟。为什么?

wpf - 当属性更改时更改自定义控件的内容模板

c# - 我正在 CTR 模式下寻找 AES-256 的 C# 实现

c# - Oracle Sequence nextval 是来回跳数

c# - Web 上下文中静态属性的影响

c# - DLL中没有可以放在工具箱上的组件

c# - Unity 4.6 编辑器,使用预定义数据创建脚本