Two-way databinding of a custom templated asp.net control
Asked Answered
I

1

6

This question was originally about getting two-way binding to work at all, but due to lack of specific answers and otherwise progress along the way, I've been updating it - You can check the edit history, but I figured this is better for clarity.

The code listing below allows a single object to be two-way databound to a templated control. I'd like to extend this example in the simplest way possible to allow for nesting of similarly two-way databinding-enabled templated controls for complex-typed properties of the root-most object. For example, SampleFormData has a property List<string> Items. I'd like to be able to bind to this list within the root-most template (from this code listing), and either display the string data in an editable list of textboxes, perhaps, with commands for insert, delete, rebind-entered-changes (back to the bound object's List property). Also, if this were a list of a complex type (SampleFormChildData, rather than string), a new embedded SampleSpecificEntryForm could be used within the list, bound to each of the list's items, like a repeater. And so on down to the leaf-simple properties, if the author so chooses. The ui-fields need not be auto-generated, just available for binding.

Note: The case of the List<string> is special because even the built-in bindings can't handle string as the DataItem directly - binding to strings directly as items in our list is not a requirement, but certainly valuable.

This is different from a FormView because it is not built to expect to bind to one of a list of items, only to a single item as persisted in viewstate or where ever. Unlike the FormView, this only has a single default template akin to FormView's EditTemplate. Likewise, binding to a collection-like property would also only have one view - edit. There is no selection of the row and then editing. Everything is editable all the time. The purpose is to make two-way bound forms easier to build.

Seems to me that there ought to be two kinds of binding. SingleEntityBinding and CollectionBinding. SingleEntityBinding takes a single object instance as a datasource (as prototyped by SampleSpecificEntryForm) while CollectionBinding could be bound to it's parent SingleEntityBinding with attributes of DataSourceID="EntryForm1" DataMember="Items" as in the code example for DataList1 below. Nesting of either type should be supported in either type. List manipulation such as insert/change/delete type operations against the backing-object's data are the responsibility of the form author; however, such mechanics would be relatively simple to implement.

Here's some code, hope it helps somebody. 200 points are out there for the best suggestions toward this laid-out goal...

using System.ComponentModel;
using System.Collections.Specialized;
using System.Collections.Generic;

namespace System.Web.UI.WebControls.Special
{
    [Serializable]
    public class SampleFormData
    {
        public string SampleString { get; set; }
        public int SampleInt { get; set; }
        public List<string> Items { get; set; }

        public SampleFormData()
        {
            SampleString = "Sample String Data";
            SampleInt = 5;
            Items = new List<string>();
        }
    }

    [ToolboxItem(false)]
    public class SampleSpecificFormDataContainer : WebControl, INamingContainer, IDataItemContainer
    {
        SampleSpecificEntryForm entryForm;

        internal SampleSpecificEntryForm EntryForm
        {
            get { return entryForm; }
        }

        [Bindable(true), Category("Data")]
        public string SampleString
        {
            get { return entryForm.FormData.SampleString; }
            set { entryForm.FormData.SampleString = value; }
        }

        [Bindable(true), Category("Data")]
        public int SampleInt
        {
            get { return entryForm.FormData.SampleInt; }
            set { entryForm.FormData.SampleInt = value; }
        }

        [Bindable(true), Category("Data")]
        public List<string> Items
        {
            get { return entryForm.FormData.Items; }
            set { entryForm.FormData.Items = value; }
        }

        internal SampleSpecificFormDataContainer(SampleSpecificEntryForm entryForm)
        {
            this.entryForm = entryForm;
        }

        #region IDataItemContainer Members
        public object DataItem { get { return entryForm.FormData; } }

        public int DataItemIndex { get { return 0; } }

        public int DisplayIndex { get { return 0; } }
        #endregion
    }

    public class SampleSpecificEntryForm : DataBoundControl, INamingContainer, IDataSource
    {
        #region Template
        private IBindableTemplate formTemplate = null;

        [Browsable(false), DefaultValue(null),
        TemplateContainer(typeof(SampleSpecificFormDataContainer), ComponentModel.BindingDirection.TwoWay),
        PersistenceMode(PersistenceMode.InnerProperty)]
        public virtual IBindableTemplate FormTemplate
        {
            get { return formTemplate; }
            set { formTemplate = value; }
        }
        #endregion

        public override ControlCollection Controls
        {
            get
            {
                EnsureChildControls();
                return base.Controls;
            }
        }

        private SampleSpecificFormDataContainer formDataContainer = null;

        [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public SampleSpecificFormDataContainer FormDataContainer
        {
            get
            {
                EnsureChildControls();
                return formDataContainer;
            }
        }

        [Bindable(true), Browsable(false)]
        public SampleFormData FormData
        {
            get
            {
                SampleFormData data = ViewState["FormData"] as SampleFormData;

                if (data == null)
                {
                    data = new SampleFormData();
                    ViewState["FormData"] = data;
                }

                return data;
            }
        }

        protected override void CreateChildControls()
        {
            if (!this.ChildControlsCreated)
            {
                this.ChildControlsCreated = true;
                Controls.Clear();
                formDataContainer = new SampleSpecificFormDataContainer(this);

                Controls.Add(formDataContainer);
                FormTemplate.InstantiateIn(formDataContainer);
            }
        }

        protected override void PerformDataBinding(Collections.IEnumerable ignore)
        {
            CreateChildControls();

            if (Page.IsPostBack)
            {
                //OrderedDictionary fields = new OrderedDictionary();

                //ExtractValuesFromBindableControls(fields, formDataContainer); // Don't know what this would be for

                foreach (System.Collections.DictionaryEntry entry in formTemplate.ExtractValues(formDataContainer))
                {
                    if (((string)entry.Key).Equals("SampleString", StringComparison.Ordinal))
                    {
                        FormData.SampleString = (string)entry.Value;
                    }

                    if (((string)entry.Key).Equals("SampleInt", StringComparison.Ordinal))
                    {
                        int i;
                        if (int.TryParse((string)entry.Value, out i))
                        {
                            FormData.SampleInt = i;
                        }
                    }
                }
            }

            formDataContainer.DataBind();
        }

        public SampleSpecificEntryForm()
        {
            this.PreRender += new EventHandler(SampleSpecificEntryForm_PreRender);
        }

        void SampleSpecificEntryForm_PreRender(object sender, EventArgs e)
        {
            SaveViewState();
        }

        #region IDataSource Members

        public event EventHandler DataSourceChanged;

        public DataSourceView GetView(string viewName)
        {
            return new PropertyView(this, viewName);
        }

        public Collections.ICollection GetViewNames()
        {
            return new List<string>() { "SampleString", "SampleInt", "Items" };
        }

        #endregion
    }

    // Not yet used ...
    public class PropertyView : DataSourceView
    {
        SampleSpecificEntryForm owner;
        string viewName;

        protected override Collections.IEnumerable ExecuteSelect(DataSourceSelectArguments arguments)
        {
            if (viewName.Equals("SampleString", StringComparison.Ordinal))
            {
                return new object[] { owner.FormData.SampleString };
            }

            if (viewName.Equals("SampleInt", StringComparison.Ordinal))
            {
                return new object[] { owner.FormData.SampleInt };
            }

            if (viewName.Equals("Items", StringComparison.Ordinal))
            {
                return new object[] { owner.FormData.Items };
            }

            throw new InvalidOperationException();
        }

        public PropertyView(SampleSpecificEntryForm owner, string viewName)
            : base(owner, viewName)
        {
            this.owner = owner;
            this.viewName = viewName;
        }
    }
}

With an ASP.NET page the following:

<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true"
    CodeBehind="Default2.aspx.cs" Inherits="EntryFormTest._Default2" EnableEventValidation="false" %>

<%@ Register Assembly="EntryForm" Namespace="System.Web.UI.WebControls.Special" TagPrefix="cc1" %>

<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
</asp:Content>
<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
    <h2>
        Welcome to ASP.NET!
    </h2>
        <cc1:SampleSpecificEntryForm ID="EntryForm1" runat="server">
    <FormTemplate>
        <asp:TextBox ID="txtSampleString" runat="server" Text='<%# Bind("SampleString") %>'></asp:TextBox><br />
        <asp:TextBox ID="txtSampleInt" runat="server" Text='<%# Bind("SampleInt") %>'></asp:TextBox><br />
        <h3>
            (<%# Container.SampleString %>, <%# Container.SampleInt %>) - aka - 
            (<%# DataBinder.Eval(Container, "SampleString")%>, <%# DataBinder.Eval(Container, "SampleInt")%>)</h3>
        <br />
        <asp:Button ID="btnUpdate" runat="server" Text="Update" /><br />
        <br />
    </FormTemplate>
</cc1:SampleSpecificEntryForm>
</asp:Content>

Default2.aspx.cs:

using System;

namespace EntryFormTest
{
    public partial class _Default2 : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            EntryForm1.DataBind();
        }
    }
}

I've implemented IDataSource as well, in an attempt to be able to nest a list component like so (within the ):

<asp:DataList ID="DataList1" runat="server" DataSourceID="EntryForm1" DataMember="Items">
    <EditItemTemplate>
        <asp:TextBox ID="TextBox3" runat="server" Text="<%# Bind(".") %>"></asp:TextBox>
    </EditItemTemplate>
    <FooterTemplate>
        <asp:Button ID="Button2" runat="server" Text="Add" CommandName="Add" />
    </FooterTemplate>
</asp:DataList>

Any thoughts on how to make this work in a cascading way would be awesome (on the Items list property, for example). One of the challenges here is that Bind() can't refer to the databound object itself (a string in this case) but on a property of that item - making binding to a list awkward.

Thanks for any help!


Discoveries along the way

Implemented IDataItemContainer. I was very hopeful that this would fix it, but no. No noticable change. Oops, implemented it on the wrong class. Now it is Binding, but the values aren't being rebound to the bound object on postback. Hmmm...

As this article suggests, Page.GetDataItem() is the source of the exception. This exception is thrown if the page's _dataBindingContext is null or empty. The article does explain this, but it doesn't say how to ensure that the Page's _dataBindingContext is populated. I'll continue looking.

As the MSDN documentation says, DataBoundControl should implement PerformDataBinding instead of overriding DataBind(). I've done so and made both-way-binding work. Is this code necessary or should I be using something built-in?

Inception answered 18/5, 2010 at 2:31 Comment(0)
H
1

Have you tried Databinder.Eval(Container.DataItem,...) syntax?

Also see this article on Bind().

PS. You need to Databind on every postback unless you are using Viewstate to preserve values.

Hogle answered 21/5, 2010 at 12:29 Comment(3)
Yes, thank you for your reply. I've tried all the various options. I've updated the post with "Discoveries along the way" with the latest info. Have you worked on this general problem before?Caloyer
Yes I've written databound custom controls, though I've been using MVC strictly for a while. PS. You need to Databind on Postback unless you are using Viewstate to preserve values.Hogle
lol, thanks Jason. But I was more concerned with being useful.Hogle

© 2022 - 2024 — McMap. All rights reserved.