Сохранение ViewState во вложенном DropDownList в пользовательском элементе управления - PullRequest
2 голосов
/ 24 мая 2019

Я создал пользовательский элемент управления (названный 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;
    }
}

Итак, вот мое замешательство ...

  1. В течение CreateChildControls, DataSource будет толькотам на оригинальном рендеринге.Поэтому я вызываю DataBind для вложенного DropDownList, чтобы он заполнялся в первый раз, а затем сохраняю все элементы управления обратно в свойство Items.
  2. Я почти уверен, что понимаю, как Items сохраняется / загружается из ViewState.
  3. Где я теряюсь, как свойство my Items затем используется для повторного заполнения DropDownList?Я подумал, что, возможно, именно тот факт, что я добавил Load\SaveViewState (который называется base.Load\SaveViewState), был тем, что действительно решило мою проблему, но когда я закомментировал все ссылки на мое свойство Items, я потерял выпадающий списокзначения снова.

Как в мире Items повторное заполнение inputControl.Items при обратной передаче?!

1 Ответ

1 голос
/ 06 июня 2019

Я понимаю, что окончательный вопрос таков:

Как в мире элементы переполняют inputControl.Items при обратной передаче?!

Тем не менее, я считаю, что это вопросна которые не нужно , чтобы (или не следует ) ответить по двум причинам:

  1. Ваше первоначальное заявление о требованиях:

    Я создал пользовательский элемент управления, который по сути оборачивает кучу надстроек-разметки вокруг DropDownList asp.net.

  2. Тот факт, что ваш код (и яСсылка на исходную версию вашего кода, которая хороша и достаточно длинна для нашего обсуждения), включает в себя множество методов, связанных с сохранением свойств настраиваемого элемента управления сложного типа в ViewState (LoadViewState, SaveViewState, Triplet, IStateManager и т. Д.), Но большинство из них не необходимы в вашем случае, потому что (и в этот момент ваше заявление о требованиях приобретает первостепенное значение):

    BootstrapDropDownэто просто составной обычайm элемент управления, который встраивает DropDownList и может (и должен) делегировать всю работу на него!

На самом деле, вы хорошо сделали это для Text и Value свойства.Почему бы не сделать это для свойства Items тоже?Ваш контроль работает по композиции .Ему не нужно поддерживать ListItemCollection своих собственных, не говоря уже о передаче его во ViewState.

И последнее, но не менее важное: очень важно помнить, что встроенные серверные элементы управления будут автоматически управлять своим собственным 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

Кроме того, я нашел ссылку на механизм IStateManager в этой статье Дино Эспозито:

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...