2

我创建了一个自定义控件(称为 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. 期间CreateChildControlsDataSource只会在原始渲染中出现。所以我调用DataBind嵌套的 DropDownList 来让它第一次填充,然后我将所有控件项存储回一个Items属性。
  2. 我很确定我了解如何Items从 ViewState 持久化/加载。
  3. 我迷路的地方是我的 Items财产如何习惯于重新填充 DropDownList?Load\SaveViewState我在想这可能是我添加(称为)的事实base.Load\SaveViewState真正解决了我的问题,但是当我注释掉对我的Items属性的所有引用时,我再次丢失了下拉列表值。

世界上如何在回发时Items重新填充?!inputControl.Items

4

1 回答 1

1

我知道最终的问题是:

世界上的项目是如何在回发时重新填充 inputControl.Items 的?!

尽管如此,我认为这是一个不需要(或不应该)回答的问题,原因有两个:

  1. 您的初始需求声明:

    我创建了一个自定义控件,它基本上将一堆 boostrap 标记包装在一个 asp.net DropDownList 周围。

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

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

事实上,您已经为TextandValue属性很好地做到了这一点。为什么不为Items财产也这样做呢?您的控制按组合工作。它不需要维护ListItemCollection自己的 a ,更不用说在 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绑定的数据。这很重要,因为向集合添加控件也是控件开始跟踪其 ViewState 的点。您可以在这篇出色的文章中阅读更多(或全部)信息:Controls

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

另外,我IStateManager在 Dino Esposito 的这篇文章中找到了对机制的参考:

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

于 2019-06-05T21:07:30.390 回答