An Unusual UpdatePanel

The code you are about to see was mostly to prove a point, to myself, and probably has limited applicability. Nonetheless, in the remote possibility this is useful to someone here it goes…

So this is a control that acts like a normal UpdatePanel where all child controls are registered as postback triggers except for a single control specified by the TriggerControlID property. You could basically achieve the same thing by registering all controls as postback triggers in the regular UpdatePanel. However with this, that process is performed automatically.

Finally, here is the code:

public sealed class SingleAsyncTriggerUpdatePanel : WebControl, INamingContainer
{
    public string TriggerControlID { get; set; }

    [TemplateInstance(TemplateInstance.Single)]
    [PersistenceMode(PersistenceMode.InnerProperty)]
    public ITemplate ContentTemplate { get; set; }

    public override ControlCollection Controls
    {
        get
        {
            this.EnsureChildControls();

            return base.Controls;
        }
    }

    protected override void CreateChildControls()
    {
        if (string.IsNullOrWhiteSpace(this.TriggerControlID))
            throw new InvalidOperationException(
                "The TriggerControlId property must be set.");

        this.Controls.Clear();

        var updatePanel = new UpdatePanel()
        {
            ID = string.Concat(this.ID, "InnerUpdatePanel"),
            ChildrenAsTriggers = false,
            UpdateMode = UpdatePanelUpdateMode.Conditional,
            ContentTemplate = this.ContentTemplate
        };

        updatePanel.Triggers.Add(new SingleControlAsyncUpdatePanelTrigger
        {
            ControlID = this.TriggerControlID
        });

        this.Controls.Add(updatePanel);
    }
}

internal sealed class SingleControlAsyncUpdatePanelTrigger : UpdatePanelControlTrigger
{
    private Control target;

    private ScriptManager scriptManager;

    public Control Target
        {
            get
            {
                if (this.target == null)
                {
                    this.target = this.FindTargetControl(true);
                }

                return this.target;
            }
        }

    public ScriptManager ScriptManager
        {
            get
            {
                if (this.scriptManager == null)
                {
                    var page = base.Owner.Page;

                    if (page != null)
                    {
                        this.scriptManager = ScriptManager.GetCurrent(page);
                    }
                }

                return this.scriptManager;
            }
        }

    protected override bool HasTriggered()
    {
        string asyncPostBackSourceElementID = this.ScriptManager.AsyncPostBackSourceElementID;

        if (asyncPostBackSourceElementID == this.Target.UniqueID)
            return true;

        return asyncPostBackSourceElementID.StartsWith(
            string.Concat(this.target.UniqueID, "$"),
            StringComparison.Ordinal);
    }

    protected override void Initialize()
    {
        base.Initialize();

        foreach (Control control in FlattenControlHierarchy(this.Owner.Controls))
        {
            if (control == this.Target)
                continue;

            bool isApplicableControl = false;
            isApplicableControl |= control is INamingContainer;
            isApplicableControl |= control is IPostBackDataHandler;
            isApplicableControl |= control is IPostBackEventHandler;

            if (isApplicableControl)
            {
                this.ScriptManager.RegisterPostBackControl(control);
            }
        }
    }

    private static IEnumerable<Control> FlattenControlHierarchy(
        ControlCollection collection)
    {
        foreach (Control control in collection)
        {
            yield return control;

            if (control.Controls.Count > 0)
            {
                foreach (Control child in FlattenControlHierarchy(control.Controls))
                {
                    yield return child;
                }
            }
        }
    }
}

You can use it like this, meaning that only the B2 button will trigger an async postback:

<cc:SingleAsyncTriggerUpdatePanel ID="Test" runat="server" TriggerControlID="B2">
    <ContentTemplate>
        <asp:Button ID="B1" Text="B1" runat="server" OnClick="Button_Click" />
        <asp:Button ID="B2" Text="B2" runat="server" OnClick="Button_Click" />
        <asp:Button ID="B3" Text="B3" runat="server" OnClick="Button_Click" />
        <asp:Label ID="LInner" Text="LInner" runat="server" />
    </ContentTemplate>
</cc:SingleAsyncTriggerUpdatePanel>
Advertisement

ASP.NET ViewState Tips and Tricks #2

If you need to store complex types in ViewState DO implement IStateManager to control view state persistence and reduce its size. By default a serializable object will be fully stored in view state using BinaryFormatter.

A quick comparison for a complex type with two integers and one string property produces the following results measured using ASP.NET tracing:

BinaryFormatter: 328 bytes in view state
IStateManager: 28 bytes in view state

BinaryFormatter sample code:

// DO NOT
[Serializable]
public class Info
{
    public int Id { get; set; }

    public string Name { get; set; }

    public int Age { get; set; }
}

public class ExampleControl : WebControl
{
    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);

        if (!this.Page.IsPostBack)
        {
            this.User = new Info { Id = 1, Name = "John Doe", Age = 27 };
        }
    }

    public Info User
    {
        get
        {
            object o = this.ViewState["Example_User"];

            if (o == null)
                return null;

            return (Info)o;
        }
        set { this.ViewState["Example_User"] = value; }
    }
}

IStateManager sample code:

// DO
public class Info : IStateManager
{
    public int Id { get; set; }

    public string Name { get; set; }

    public int Age { get; set; }

    private bool isTrackingViewState;

    bool IStateManager.IsTrackingViewState
    {
        get { return this.isTrackingViewState; }
    }

    void IStateManager.LoadViewState(object state)
    {
        var triplet = (Triplet)state;

        this.Id = (int)triplet.First;
        this.Name = (string)triplet.Second;
        this.Age = (int)triplet.Third;

    }

    object IStateManager.SaveViewState()
    {
        return new Triplet(this.Id, this.Name, this.Age);
    }

    void IStateManager.TrackViewState()
    {
        this.isTrackingViewState = true;
    }
}

public class ExampleControl : WebControl
{
    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);

        if (!this.Page.IsPostBack)
        {
            this.User = new Info { Id = 1, Name = "John Doe", Age = 27 };
        }
    }

    public Info User { get; set; }

    protected override object SaveViewState()
    {
        return new Pair(
            ((IStateManager)this.User).SaveViewState(),
            base.SaveViewState());
    }

    protected override void LoadViewState(object savedState)
    {
        if (savedState != null)
        {
            var pair = (Pair)savedState;

            this.User = new Info();

            ((IStateManager)this.User).LoadViewState(pair.First);

            base.LoadViewState(pair.Second);
        }
    }
}

ASP.NET ViewState Tips and Tricks #1

In User Controls or Custom Controls DO NOT use ViewState to store non public properties.

Persisting non public properties in ViewState results in loss of functionality if the Page hosting the controls has ViewState disabled since it can no longer reset values of non public properties on page load.

Example:

public class ExampleControl : WebControl
{
    private const string PublicViewStateKey = "Example_Public";
    private const string NonPublicViewStateKey = "Example_NonPublic";

    // DO
    public int Public
    {
        get
        {
            object o = this.ViewState[PublicViewStateKey];

            if (o == null)
                return default(int);

            return (int)o;
        }
        set { this.ViewState[PublicViewStateKey] = value; }
    }

    // DO NOT
    private int NonPublic
    {
        get
        {
            object o = this.ViewState[NonPublicViewStateKey];

            if (o == null)
                return default(int);

            return (int)o;
        }
        set { this.ViewState[NonPublicViewStateKey] = value; }
    }
}

// Page with ViewState disabled
public partial class ExamplePage : Page
{
    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);

        this.Example.Public = 10; // Restore Public value
        this.Example.NonPublic = 20; // Compile Error!
    }
}

Razor – Hiding a Section in a Layout

Layouts in Razor allow you to define placeholders named sections where content pages may insert custom content much like the ContentPlaceHolder available in ASPX master pages.

When you define a section in a Razor layout it’s possible to specify if the section must be defined in every content page using the layout or if its definition is optional allowing a page not to provide any content for that section. For the latter case, it’s also possible using the IsSectionDefined method to render default content when a page does not define the section.

However if you ever require to hide a given section from all pages based on some runtime condition you might be tempted to conditionally define it in the layout much like in the following code snippet.

if(condition) {
	@RenderSection("ConditionalSection", false)
}

With this code you’ll hit an error as soon as any content page provides content for the section which makes sense since if a page inherits a layout then it should only define sections that are also defined in it.

To workaround this scenario you have a couple of options. Make the given section optional with and move the condition that enables or disables it to every content page. This leads to code duplication and future pages may forget to only define the section based on that same condition.

The other option is to conditionally define the section in the layout page using the following hack:

@{
    if(condition) {
        @RenderSection("ConditionalSection", false)
    } 
    else {
        RenderSection("ConditionalSection", false).WriteTo(TextWriter.Null);
    }
}

Hack inspired by a recent stackoverflow question.

Assembly Resources Expression Builder

In ASP.NET you can tackle the internationalization requirement by taking advantage of native support to local and global resources used with the ResourceExpressionBuilder.

But with this approach you cannot access public resources defined in external assemblies referenced by your Web application.

However, since you can extend the .NET resource provider mechanism and create new expression builders you can workaround this limitation and use external resources in your ASPX pages much like you use local or global resources.

Finally, if you are thinking, okay this is all very nice but where’s the code I can use? Well, it was too much to publish directly here so download it from Helpers.Web@Codeplex, where you also find a sample on how to configure and use it.

Searching With the Entity Framework Data Source

A search mechanism is something unavoidable for most applications. A common scenario is to provide the user a set of filters for which he can supply values and incrementally restrict the number of records that satisfy these restrictions.

Search Form

It’s also normal for the user not to use all the filters supported by the search form which means that the unused filters must be excluded from the search query to not affect the result. A quick solution to this problem is to generate this search query dynamically and omit the empty filters.

However in this example I will be taking advantage of the fact that SQL Server supports expression short-circuiting to use an Entity Framework data source control with a static Where clause.

<asp:EntityDataSource ID="eds" runat="server"
    ConnectionString="name=EnContext" DefaultContainerName="EnContext"
    EntitySetName="ProductSet"
    Select="it.ID, it.Name, it.Type, it.Discontinued"
    AutoGenerateWhereClause="false"
    Where="(@NameFilterEmpty OR it.Name LIKE @NameFilter) AND (@TypeFilterEmpty OR it.Type = @TypeFilter) AND (@DiscontinuedFilterEmpty OR it.Discontinued = @DiscontinuedFilter)">
  <WhereParameters>
    <My:ControlFormatParameter Name="NameFilter" ControlID="nameTB"
      DbType="String" Format="%{0}%" />
    <My:EmptyControlCheckParameter Name="NameFilterEmpty"
      ControlID="nameTB" DbType="Boolean" />
    <asp:ControlParameter Name="TypeFilter" ControlID="typeDD"
      DbType="Int32" PropertyName="SelectedValue" />
    <My:EmptyControlCheckParameter Name="TypeFilterEmpty"
      ControlID="typeDD" DbType="Boolean"
      PropertyName="SelectedValue" EmptyValue="-1" />
    <asp:ControlParameter Name="DiscontinuedFilter"
      ControlID="discontinuedRBL" DbType="Boolean" />
    <My:EmptyControlCheckParameter Name="DiscontinuedFilterEmpty"
      ControlID="discontinuedRBL" DbType="Boolean" />
  </WhereParameters>
</asp:EntityDataSource>

As you see in the previous code snippet each search filter is controlled by a specific boolean parameter that when true disables the associated filter. The classes ControlFormatParameter and EmptyControlCheckParameter both derive from ASP .NET ControlParameter and allow in the first case to format the value of the associated input control before passing it to the data source and in the second case to switch each search filter on/off by evaluating if the associated input control has an empty value.

public class ControlFormatParameter : ControlParameter
{
    public string Format { get; set; }

    protected override object Evaluate(HttpContext context, Control control)
    {
        return String.Format(this.Format, base.Evaluate(context, control));
    }
}

public class EmptyControlCheckParameter : ControlParameter
{
    public EmptyControlCheckParameter()
    {
        this.EmptyValue = string.Empty;
    }

    public string EmptyValue { get; set; }

    protected override object Evaluate(HttpContext context, Control control)
    {
        string value = base.Evaluate(context, control).ToString();

        if (String.Equals(value, this.EmptyValue, StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }

        return false;
    }
}