in

Corey Roth and Friends Blogs

Group site for developer blogs dealing with (usually) Ionic, .NET, SharePoint, Office 365, Mobile Development, and other Microsoft products, as well as some discussion of general programming related concepts.

Cory Robinson's Blog

  • Varying OutputCache by URL in Sitecore

    I know it has been quite some time since my last blog post, but I thought this was worth writing down.  The goal was to vary our header html caching by URL.  The OutputCache directive in ASP.NET does not provide this option out of the box, so I had to do a VaryByCustom.  VaryByParam is required so we set it to None.  The VaryByCustom value is just an arbitrary key that you come up with.  Here is the OutputCache directive we used for our header user control:

    <%@ OutputCache Duration="60" Shared="true" VaryByParam="None" VaryByCustom="sitecoreurl" %>

    In order to use VaryByCustom, one must provide a GetVaryByCustomString override in Global.asax.   The context parameter can be used to get context.Request.Url, but in Sitecore, this is set to the URL of the Sitecore layout that is used for the content page, and not the actual resulting URL after Sitecore processes it.  So we had to use the FriendlyUrl from the Sitecore item to get the actual result URL for the page.

            public override string GetVaryByCustomString(HttpContext context, string arg)

            {

                if (arg == "sitecoreurl")

                {

                    if (Sitecore.Context.Item != null)

                        return Sitecore.Context.Item.Paths.GetFriendlyUrl();

                    else

                        return string.Empty;

                }

                else

                {

                    return string.Empty;

                }

            }

     

    The result is that our header html caches different versions of the html by URL, so the html for /Products.aspx and /FAQs.aspx would be cached separately.

  • HtmlEncodeFormatString errors in GridView BoundField after precompile

    I ran across an issue today where my data format string was not being applied to my data in a gridview.  It worked locally, but not in dev or test.  Here's what I had:

    <asp:BoundField DataField="GrandTotal" HeaderText="Grand Total" ReadOnly="True" SortExpression="GrandTotal" DataFormatString="{0:c}" />

     

    Ok what would make this render differently in dev?  I looked at the HtmlEncodeFormatString property of the BoundField, noticing that setting it to false made the formatting go away locally as well.  So I assumed that I needed to add HtmlEncodeFormatString="true" in dev to make it work.  However, I got the following error message:

     

    Error: Type 'System.Web.UI.WebControls.BoundField' does not have a public property named 'HtmlEncodeFormatString'.

     

    No idea why this is happening.  I googled the error but found nothing useful.  There is an issue with ASP.NET 3.5 and this property defaulting to true, but I don't have 3.5 installed on the dev box.  So that can't be it.

     

    So I had the bright idea to convert this BoundField to a template column (by clicking the linkbutton in the Edit Columns designer).  I got this:

     

    <asp:TemplateField HeaderText="Grand Total" SortExpression="GrandTotal">

      <ItemTemplate>

        <asp:Label ID="Label1" runat="server" Text='<%# Bind("GrandTotal", "{0:c}") %>'></asp:Label>

      </ItemTemplate>

    </asp:TemplateField>

     

    Magically, this works both locally and in dev.  I have no idea why.

  • Sorting and Paging GridViews with Custom Object Collections

     

        protected void PendingWorkGridView_OnSorting(object sender, GridViewSortEventArgs e)

        {

            // For some reason the SortDirection property is not getting set automatically so set manually

            e.SortDirection = e.SortExpression.IndexOf(" DESC") > 0 ? SortDirection.Descending : SortDirection.Ascending;

     

            List<MyWorklistItem> pendingWorkItems = (List<MyWorklistItem>) Session["MyWorkItems"];

     

            pendingWorkItems.Sort(new MyWorklistItemComparer(e.SortBLOCKED EXPRESSION;

     

            // Update SortExpression for the column, toggling between ASC and DESC

            foreach (DataControlField column in PendingWorkGridView.Columns)

            {

                if (column.SortExpression == e.SortExpression)

                {

                    column.SortExpression = e.SortDirection == SortDirection.Ascending

                                                ? e.SortExpression + " DESC"

                                                : e.SortExpression.Replace(" DESC", string.Empty);

     

                    break;

                }

            }

     

            PendingWorkGridView.DataSource = pendingWorkItems;

            PendingWorkGridView.DataBind();

        }

     

        protected void PendingWorkGridView_OnPageIndexChanging(object sender, GridViewPageEventArgs e)

        {

            PendingWorkGridView.PageIndex = e.NewPageIndex;

            PendingWorkGridView.DataBind();

        }

  • Restricting authentication to aspx pages in your authentication module

    In your httpmodule that you are using for authentication, in the handler for the AuthenticateRequest event, include these lines:

     

       34 HttpApplication httpApp = (HttpApplication) sender;

       35 

       36             // Only authenticate for aspx pages

       37             if (Path.GetExtension(HttpContext.Current.Request.Url.LocalPath.ToLower()) != ".aspx")

       38             {

       39                 HttpContext.Current.Response.StatusCode = 200;

       40                 HttpContext.Current.SkipAuthorization = true;

       41                 return;

       42             }

  • How to make UNC paths with spaces link properly in emails

    In order for a unc path like

    \\machinename\path with spaces\

    link correctly in a text only email, surround it with < and > like

    <\\machinename\path with spaces\>

  • Code Readability => Code Maintainability

    As developers the readability of our code is what determines how easily either we ourselves or other developers can maintain our code.  Sometimes we can accomplish the same functionality in fewer lines of code, or using some fancy tricks, but this can be bad for maintainability.  Sure, there are some situations where some of the code is repeated unnecessarily, or may not be needed at all, and can be made both simpler and easier to read by refactoring it.  But rewriting readable code in fewer lines such that anyone who looks at it will spend considerable time understanding what it is doing is no good.

     

    Extensive use of the ternary conditional operator is one sign your code might not be as readable as it should be.  For example, the following code works correctly and in my opinion is pretty readable as to what its intended functionality is supposed to be:

     

    if (!productStartAdded)

    {

    // Add with an OR unless worklist criteria count is 0

    if (filterCount == 0)

    {

    worklistCriteria.AddFilterField(WCLogical.And, WCField.ProcessStatus, WCCompare.Equal, k2ProcessInstanceStatuses[j]);

    filterStringBuilder.Append(" && ProcessStatus == " + k2ProcessInstanceStatuses[j].ToString());

    }

    else

    {

    worklistCriteria.AddFilterField(WCLogical.Or, WCField.ProcessStatus, WCCompare.Equal, k2ProcessInstanceStatuses[j]);

    filterStringBuilder.Append(" || ProcessStatus == " + k2ProcessInstanceStatuses[j].ToString());

    }

     

    productStartAdded = true;

    }

    else

    {

    worklistCriteria.AddFilterField(WCLogical.And, WCField.ProcessStatus, WCCompare.Equal, k2ProcessInstanceStatuses[j]);

    filterStringBuilder.Append(" && ProcessStatus == " + k2ProcessInstanceStatuses[j].ToString());

    }

     

    Note that the example code above is using the K2ROM API for K2.NET.  If there were an overload of the AddFilterField method that took just the And or Or, the code would be simplifyable and retain its readability but unfortunately there is no such overload.

     

    But say we decide that we can do some fancy maneuvering and rewrite these 19 lines of code in just three lines!  Wow, that would be cool, right?  Take a look at those three lines, which have the same functionality as the original 19:

     

    worklistCriteria.AddFilterField(productStartAdded && filterCount != 0 ? WCLogical.And : WCLogical.Or, WCField.ProcessStatus, WCCompare.Equal, k2ProcessInstanceStatuses[j]);

     

    filterStringBuilder.Append((productStartAdded && filterCount != 0 ? " && " : " || ") + " ProcessStatus == " + k2ProcessInstanceStatuses[j].ToString());

     

    productStartAdded = true;

     

    That is downright amazing.  The compiler is going to thank us for that, right?  Or is it?  Well, the original version results in 91 lines of IL code, while the rewritten version is just 49 lines of IL.  That is certainly not the roughly 6:1 ratio that we saw in the C# code, nor is it even 2:1 in IL.

     

    One might expect about a 46% performance boost based on those IL line counts.  To test this, I actually profiled the code in the ANTS Profiler, executing each method 1000000 times. The results showed no performance boost at all, rather a slight decrease in performance:

     

    Original: 12.2521 seconds                    Rewritten: 12.8333 seconds

     

    Amazing!  Or is it?  When you look at what the code is actually doing, the original version is going to have 3-5 of its lines executed depending on the value of productStartAdded and filterCount.  The rewritten version will always have all three of its lines executed.  So the performance metrics that we obtained now make sense.

     

    So now that we have painstakenly shown that, even though you think it will impress your friends, rewriting the 19 lines of code into three lines doesn’t help performance a bit.  What should we then conclude about this result?

     

    The conclusion that I take away from this is that, since the performance is basically the same in both cases, I would rather see the original 19 lines of code that I can read and determine easily what the purpose of the code is, versus seeing the rewritten three lines of code and sitting there scratching my head for an hour trying to decipher these cryptic statements.  So would your fellow developers who are going to maintain your code.

  • Implementing a Service Locator

    One of the problems with using a factory is that typically you have to change your factory when you add new classes of objects that can be created. I decided that this was not good enough, so instead used a configuration file to list out the types that the factory can create. This is commonly referred to as a Service Locator.  This post describes how to implement one.

    This uses the System.Activator class to create objects of the specified type at runtime. The configuration file just uses AppSettings to store key/value pairs of type names and fully qualified (with namespace) type names. As more classes are added that need to be created by this factory, all that is required is to add a new configuration file key/value pair for each new class.

    Since this is done at runtime, I would assume there is a performance hit doing it this way. But it's yet another tradeoff between extensibility and performance. What is right for your specific application will of course depend on the level of performance that you need.

    This example will use an abstract Report class and three concrete classes that derive from Report.

    First of all, we have the abstract class which all classes we will instantiate derive from:

    abstract class Report

    {

    protected ReportViewer reportViewer;

    protected ReportCreationParameters creationParameters;

           

    protected Report(ReportCreationParameters parameters)

    {

    creationParameters = parameters;           

    }

     

    protected abstract void DisplayReport (ReportDisplayParameters parameters);

    }

     

    Now an example of a derived class:

     

    class ReportTypeA : Report

    {

    public ReportTypeA(ReportCreationParameters parameters) : base(parameters)

    {

    // Create report based on the parameters passed in

    }

     

    protected override void DisplayReport(ReportDisplayParameters parameters)

    {

    // Display method specific to report type A

    }

    }

     

    The classes referred to above are very simple, and will use Reporting Services types and settings eventually.  The ReportCreationParameters class contains settings that will be used to create the report, while the ReportDisplayParameters class contains settings that will be used to display the report.

     

    class ReportViewer { }

     

    class ReportCreationParameters

    {

    private string parameter1;

    private string parameter2;

     

    public string Parameter1

    {

    get { return parameter1; }

    set { parameter1 = value; }

    }

     

    public string Parameter2

    {

    get { return parameter2; }

    set { parameter2 = value; }

    }

    }

     

    class ReportDisplayParameters { }

     

    Here is the AppSettings section of the configuration file:

     

      <appSettings>

        <add key="ReportTypeA" value="DotNetMafia.ReportTypeA"/>

        <add key="ReportTypeB" value="DotNetMafia.ReportTypeB"/>

        <add key="ReportTypeC" value="DotNetMafia.ReportTypeC"/>

      appSettings>

     

    Now the interesting part, the factory, which uses System.Activator and AppSettings to instantiate the types specified in the configuration file. I used the CreateInstance overload that takes the class type and an array of objects, which are the parameters to the constructor (ReportCreationParameters in this example):

     

    static class ReportFactory

    {

    public static Report CreateReport(string reportType, ReportCreationParameters parameters)

    {

    string reportClassType = ConfigurationManager.AppSettings[reportType];

    return (Report)Activator.CreateInstance(Type.GetType(reportClassType), new object[] { parameters });

    }

    }

     

    So then from the main program I use this code to instantiate whatever class I need:

     

    ReportCreationParameters parameters = new ReportCreationParameters();

    parameters.Parameter1 = "TypeAParameter1";

    parameters.Parameter2 = "TypeAParameter2";

    ReportFactory.CreateReport("ReportTypeA", parameters);

     

    So there you have it.  This is nothing profound or even original, but in my opinion it is a good way to use factories with extensibility in mind.

  • Validating Custom Composite Web Controls

    Have you ever wondered how you go about validating a custom composite web control?  Normally, with ASP.NET server controls, you have:


    <asp:TextBox ID="PhoneTextBox" runat="server" />
    <asp:RequiredFieldValidator ID="PhoneRequiredFieldValidator" runat="server" ControlToValidate="PhoneTextBox" Display="Dynamic"
    ErrorMessage="Please enter a phone number" />


    But with a custom composite web control, say one which includes a label, a text box, and a required field validator, any validators that refer to your control will not know how to validate the data.


    Say we have the following custom composite web control:


    public class LabelledTextBox : CompositeControl
    {
        private Label _nameLabel = new Label();
        private TextBox _nameTextBox = new TextBox();
        private RequiredFieldValidator _nameRequiredFieldValidator = new RequiredFieldValidator();

        public LabelledTextBox()
        {
            _nameLabel.ID = "NameLabel";
            _nameLabel.Text = "My Label: ";

            _nameTextBox.ID = "NameTextBox";

            _nameRequiredFieldValidator.ID = "NameRequiredFieldValidator";
            _nameRequiredFieldValidator.ControlToValidate = _nameTextBox.ID;
        }

        protected override void CreateChildControls()
        {
            Controls.Add(_nameLabel);
            Controls.Add(_nameTextBox);
            Controls.Add(_nameRequiredFieldValidator);

            base.CreateChildControls();
        }

        public string Text
        {
            get { return _nameTextBox.Text; }
            set { _nameTextBox.Text = value; }
        }
    }


    The RequiredFieldValidator inside the composite control can validate the text box just fine, as it knows about it. However, if you want to validate that the text box has, say, an integer between 1 and 100, by adding a RangeValidator to the markup, the validation will not work. You will get an error stating that the LabelledTextBox control cannot be validated. So, after some failed google attempts to find out how to solve this problem, my coworker Bryan Smith recalled that in Fritz Onion's book, Essential ASP.NET With Examples in C#, Fritz mentioned the ValidationProperty attribute that could be used to specify which property of the composite control should be validated.


    So if we just add that attribute to the custom composite control class, we can then validate it:


    [ValidationProperty("Text")]
    public class LabelledTextBox : CompositeControl


    So now when we have the following in our markup, it will validate the text box just fine:


    <asp:LabelledTextBox ID="AgeLabelledTextBox" runat="server"/>
    <asp:RangeValidator ID="AgeValidator" runat="server" ControlToValidate="AgeLabelledTextBox" Display="Dynamic"
    ErrorMessage="Please enter an age between 1 and 100" Type="Integer"MinimumValue="1" MaximumValue="100" />


    Thanks to the ValidationProperty, the framebwork now knows that the Text property of the custom composite control should be checked to make sure the value is between 1 and 100.


    But there is a gotcha here.  The client side validation will appear to work just fine with the above scenario.  But if you have two text boxes within the same composite control,only the first one will be validated. The reason for this, as Bryan and I discovered, is that the validation javascript actually iterates through the child nodes of the composite control(rendered as a span) until it sees a control that has a value property.  It then validates that control and that control only.


    So again, always validate your input at both client AND server sides.

2019.
Powered by Community Server (Non-Commercial Edition), by Telligent Systems