Set dynamic value for property of ASP.Net Control

1.2k views Asked by At

I'm trying to pass a dynamic variable into a control:

<% foreach(var product in CartModel.Products) { %>
    <kc:Warning runat="server" id="Warning" ProductId="<%= product.ProductId %>" />
%>

but when I access ProductId in the control (in /ProductAvailabilityWarning.ascx.cs), it's being passed as literally "<%= product.ProductId %>", not as the value of product.ProductId.

I'd rather not rewrite all of my code as a Repeater, since there's a ton of dynamic code inside the foreach block that I'd have to modify.

EDIT: Since this code appears to be confusing, due to being a Frankenstein monstrosity of legacy code, here's a more complete snippet of the code, and why I'm trying to avoid rewriting the whole thing as a Repeater

<% foreach(var product in CartModel.Products) { %>
<kc:Warning runat="server" id="Warning" ProductId="<%= product.ProductId %>" />

<div class="cart-product js-cart-product">
    <div class="cart-product__product-details">
        <div class="cart-product__name"><%= product.ProductName %></div>
        <div class="cart-product__details">
            <div class="cart-product__detail-label"><%= product.MaturityLabel %></div>
            <div class="cart-product__detail-description"><%= product.RelativeMaturity %> <%= Dictionary.GetValue("eBusiness", "days" ) %></div>
            <ul class="cart-product__detail-icons">
                <% foreach(var maturityIcon in product.MaturityIcons) { %>
                <li>
                    <img
                        src="<%= maturityIcon.Src %>"
                        alt="" />
                    <span class="badge-label"><%= maturityIcon.Alt %></span>
                </li>
                <% } %>
            </ul>
        </div>
        <div class="cart-product__treatments-label">
            <%= Dictionary.GetValue("eBusiness", "seed treatments selected" ) %>
        </div>
        <ul class="cart-product__treament-grid">
                <% foreach(var seedTreatmentImage in product.SeedTreatmentImages) { %>
                <li class="cart-product__selected-treatment"> 
                    <img
                    src="<%= seedTreatmentImage.Src %>"
                    alt="<%= seedTreatmentImage.Alt %>" />
                </li>
                <% } %>
        </ul>
    </div>
    <div class="cart-product__order-totals">
        <% if (product != null && product.ProductBagImage != null) { %>
        <div class="cart-product__logo">
            <img width="325" height="500"
                src="<%= product.ProductBagImage.Src %>"
                alt="<%= product.ProductBagImage.Alt %>" />
        </div>
        <% } %>
        <div class="cart-product__quantity">
            <span class="cart-product__value"><%= product.Quantity %></span>
            <% if (product.Size == eBusiness.Sizes.Bag) { %>
            <span class="cart-product__label"><%= Dictionary.GetValue("eBusiness", "size_bags" ) %></span>
            <% } else if (product.Size == eBusiness.Sizes.Tote) { %>
            <span class="cart-product__label"><%= Dictionary.GetValue("eBusiness", "size_totes" ) %></span>
            <% } else if (product.Size == eBusiness.Sizes.SeedPak) { %>
            <span class="cart-product__label"><%= Dictionary.GetValue("eBusiness", "size_seedpaks" ) %></span>
            <% } %>
        </div>
        <div class="cart-product__acres">
            <span class="cart-product__value"><%= product.Acres %></span>
            <span class="cart-product__label"><%= Dictionary.GetValue("eBusiness", "acres") %></span>
        </div>
        <div class="cart-product__divider"></div>
        <div class="cart-product__total">
            <sup class="cart-product__currency">$</sup>
            <span class="cart-product__value"><%= product.Msrp.ToString("0,0.00") %></span>
            <span class="cart-product__label"><%= Dictionary.GetValue("eBusiness", "MSRP") %></span>
        </div>
    </div>
    <% if(ShowRemoveButton){ %>
    <div class="cart-product__remove">
        <button
            class="cart-product__cta-button js-cart-product__remove"
            type="button"
            data-bundle-id="<%= product.BundleId %>">
            <img src="/build/img/svg-sprite/icon-garbage.svg" alt="" />
            <span><%= Dictionary.GetValue("eBusiness", "remove") %></span>
        </button>
    </div>
    <% } %>
    <% if(ShowEditButton){ %>
    <div class="cart-product__edit">
        <a href='<%= ProductCustomizationBaseUrl + product.ProductId + "&c=" + product.BundleId %>' class="cart-product__cta-button">
            <img src="/build/img/icon-edit.svg" alt="" />
            <span><%= Dictionary.GetValue("eBusiness", "edit") %></span>
        </a>
    </div>
    <% } %>
</div>
<% } %>
1

There are 1 answers

0
Dai On
  • Remove the <kc:Warning/> control syntax, controls don't work like that.
    • Under WebForms, controls are instantiated in Init (IIRC), then have their properties set during __VIEWDATA's deserialization events and again in the data-binding events (but only for <%# %>-style syntax).
    • The <%= %> syntax denotes a call to HtmlTextWriter.Write() which simply writes the inner string value to the output stream, and it's only invoked during the page's render-function: so the <%= %> syntax cannot be used to set properties of controls (only <%# %> can be used for that, but only if you're using WebForms' (horribly complicated) data-binding system.
  • If you want to load a UserControl (aka .ascx) and just use it render HTML without faffing around with WebForm's control lifecycle and old-and-horrible data-binding system the good news is: you can!
    • There is a tiny bit of legwork involved, but it's straightforward to understand, and that's is what this answer is all about.

Like so:

  1. Use Page.LoadControl(String virtualPath) to get a Control object instance representing your user-control.
    • The virtualPath argument will be a const String like ~/MyControls/Warning.ascx or similar.
  2. Cast the result to your specific UserControl subclass (the type defined in your .ascx's code-behind file.
  3. Then inside the loop body set the properties then call RenderControl, passing the (hidden) __w object from your .aspx's render function.

Like so:

MyPage.aspx:

<%
    MyWarningControl warningCtrl = (MyWarningControl)this.LoadControl("~/MyControls/Warning.ascx");
%>

<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. </p>

<%    foreach(var product in CartModel.Products) { %>
<%
    warningCtrl.ProductId = product.ProductId;
    warningCtrl.AnythingElse = product;

    warningCtrl.RenderControl( __w );
%>

<div class="cart-product js-cart-product">

<!-- etc --> 
</div>

<% } /*foreach product*/ %>

You can simplify things somewhat if you add a custom Render function to your UserControl's code-behind class, like so:

Also, you can also use ASP.NET Core-style constructor-dependency injection into your UserControl's if you're feeling adventurous (and are running on .NET Framework 4.7.2 or later).

public static class LoadingExtensions
{
    public static MyWarningControl LoadMyWarningControl( this Page p )
    {
        MyWarningControl warningCtrl = (MyWarningControl)p.LoadControl( "~/MyControls/Warning.ascx" );
        return warningCtrl;
    }
}

public partial class MyWarningControl : UserControl
{
    public void RenderProduct( HtmlTextWriter w, Product p )
    {
        if( w is null ) throw new ArgumentNullException( nameof(w) );

        try
        {
            this.Product = p ?? throw new ArgumentNullException( nameof(p) );

            this.RenderControl( w );
        }
        finally
        {
            this.Product = null;
        }
    }

    public Product Product { get; private set; }

    public Int32 ProductId => this.Product.ProductId;
}

Then your .aspx logic simplifies down to just this:

<%
    MyWarningControl warningCtrl = this.LoadMyWarningControl();
%>

<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. </p>

<%    foreach(var product in CartModel.Products) { %>
<%
   warningCtrl.RenderProduct( __w, product );
%>

<div class="cart-product js-cart-product">

<!-- etc --> 
</div>

<% } /*foreach product*/ %>

Much simpler!


If you're wondering where __w comes from: it's the HtmlTextWriter where your HTML is being written to. It's passed as a parameter into the render function generated from your .aspx file.

You can see it yourself by loading the .dll file generated by aspnet_compiler.exe into ILSpy and locating the generated __Render_controlN method in your Page subclass, for example:

enter image description here

So every occurrence of <%= x %> and <%: y %> is converted to __w.Write(x); and __w.Write(HttpUtility.HtmlEncode(y)); respectively.

...which also explains why <asp:SomeControl runat="server" PropertyName="<%= x %>" /> isn't allowed: because <asp:SomeControl is an object with its own logic and render function, and not just plaintext that's passed through HtmlTextWriter.