sebnilsson.com | Liquid Development Is What I Do
Seb Nilsson

Render .ascx-Files in ASP.NET MVC Using Only RazorViewEngine

ASP.NET Band-Aid

If you're stuck in an environment where you're migrating from ASP.NET MVC to ASP.NET WebForms it's good to know that you can actually render your existing WebForms-Controls in you MVC-views. This might sound like a crazy thing to do (and it is in the long run!) but it might be useful if you're stuck between sprints and have perfectly working WebForms-Controls (.ascx-files) that you don't have time to migrate right now. All you have to do is use the HtmlHelper's helper-method .RenderPartial(string partialViewName) and pass it the path to the WebForms-Control.

// Write the content of a control inside a view:
Html.RenderPartial("~/Controls/ControlVirtualPath.ascx");

// Or to get the content of a control as a MvcHtmlString, for further manipulation:
Html.Partial("~/Controls/CustomControl.ascx")

It's important that your controls inherits from System.Web.Mvc.ViewUserControl and NOT the old System.Web.UI.UserControl.

One performance-tip that is often mentioned around ASP.NET MVC is to deactivate the WebForms-View Engine for MVC Razor-views (which actually turns out to maybe not make such a big difference after all). This will not prevent .aspx, .ascx and other WebForms-files from working.

But you still want your .ascx-files to work inline in your MVC Razor-views. This can be achieved by implementing your own class that inherits RazorViewEngine, which only uses the WebFormViewEngine when actually needed.

Global.asax (or other config-class)

// Remove WebFormViewEngine (and RazorViewEngine)
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new CustomRazorViewEngine());

CustomRazorViewEngine : System.Web.Mvc.RazorViewEngine

private static readonly WebFormViewEngine WebFormsEngine = new WebFormViewEngine();

public override ViewEngineResult FindPartialView(
    ControllerContext context, string name, bool useCache)
{
    if (name.EndsWith(".ascx"))
    {
        return WebFormsEngine.FindPartialView(context, name, useCache);
    }

    return base.FindPartialView(context, name, useCache);
}

If you need the actual class of the control to do some further analysis/manipulation, you can do the following anywhere in your code:

var viewPage = new ViewPage();
var control = viewPage.LoadControl("~/Controls/ControlVirtualPath.ascx") as ControlType;

ASP.NET WebForms SEO: Compressing View State

In my previous post about Moving View State to Bottom of the Form I touched on one way to do some Search Engine Optimization in ASP.NET WebForms. Another thing you can do is to compress large View States on a page.

All the code in this article should be put in a class inheriting from System.Web.UI.Page. Like a page's Code-behind file or your own PageBase-class.

First we need some common logic for both loading and saving a compressed View State, the name of the hidden field to use.

private const string ViewStateFieldName = "SEOVIEWSTATE";

We start with the loading-part, where you see the use of my implementation of the CompressionHelper-class, which simplifies compression. It also solves the issue with DeflateStream and GZipStream inflating already compressed data, which is resolved in .NET Framework 4.

protected override object LoadPageStateFromPersistenceMedium() {
    return LoadCompressedPageState();
}

private object LoadCompressedPageState() {
    string viewState = Request.Form[ViewStateFieldName];
    if(string.IsNullOrEmpty(viewState)) {
        return string.Empty;
    }

    byte[] bytes = Convert.FromBase64String(viewState.Substring(1));

    bool isCompressed = Convert.ToBoolean(Convert.ToInt32(viewState.Substring(0, 1)));
    if(isCompressed) {
        bytes = CompressionHelper.Decompress(bytes);
    }

    string decompressedBase64 = Convert.ToBase64String(bytes);

    ObjectStateFormatter formatter = new ObjectStateFormatter();
    return formatter.Deserialize(decompressedBase64);
}

We then move on to the implementation of saving the page's state.

protected override void SavePageStateToPersistenceMedium(object state) {
    SaveCompressedPageState(state);
}

private void SaveCompressedPageState(object state) {
    byte[] viewStateBytes;
    using(MemoryStream stream = new MemoryStream()) {
        ObjectStateFormatter formatter = new ObjectStateFormatter();
        formatter.Serialize(stream, state);
        viewStateBytes = stream.ToArray();
    }

    byte[] compressed;
    bool successfulCompress = CompressionHelper.TryCompress(viewStateBytes, out compressed);
    string compressedBase64 =
        Convert.ToInt32(successfulCompress) + Convert.ToBase64String(compressed);

    ClientScript.RegisterHiddenField(ViewStateFieldName, compressedBase64);
}

So the summary is that this will compress the View State of an ASP.NET WebForms page, if the View State is large enough. Otherwise it will just keep it the way it is.

ASP.NET WebForms SEO: Moving View State to Bottom of the Form

As we all know, SEO is very important for almost all sites and some of us are still stuck with sites based on ASP.NET WebForm. But there are still things you can do to optimize these sites.

Since Google and other search-engines only indexes X amount of KB of a page, you don't want half of that content to be meaningless View State (which you should TRULY understand by now).

One solution is to compress the View State, but in this article I will show you how to move it to the bottom of the Form-tag. To achieve the latter, I will use a HttpModule, which will intercept all requests to files served through ASP.NET in the IIS.

First we need to hook in to the BeginRequest-event to only intercept requests to .aspx-pages.

public class ViewStateSeoHttpModule : IHttpModule {
    public void Init(HttpApplication context) {
        context.BeginRequest += new EventHandler(BeginRequest);
    }

    private void BeginRequest(object sender, EventArgs e) {
        HttpApplication application = sender as HttpApplication;

        bool isAspNetPageRequest = GetIsAspNetPageRequest(application);
        if(isAspNetPageRequest) {
            application.Context.Response.Filter =
                new ViewStateSeoFilter(application.Context.Response.Filter);
        }
    }

    private bool GetIsAspNetPageRequest(HttpApplication application) {
        string requestInfo = application.Context.Request.Url.Segments.Last();
        bool isAspNetPageRequest = requestInfo.ToLowerInvariant().Contains(".aspx");
        return isAspNetPageRequest;
    }
    // [...]

What we do here is add a Reponse Filter which moves the View State to the bottom of the Form. I made this to an internal class called ViewStateSeoFilter.

internal class ViewStateSeoFilter : Stream {
    private string _topPageHiddenFields;
    private Stream _originalFilter;

    public ViewStateSeoFilter(Stream originalFilter) {
        _originalFilter = originalFilter;
    }

    public override bool CanRead { get { return true; } }
    public override bool CanSeek { get { return true; } }
    public override bool CanWrite { get { return true; } }
    public override long Length { get { return 0; } }
    public override long Position { get; set; }

    public override void Flush() {
        _originalFilter.Flush();
    }

    public override int Read(byte[] buffer, int offset, int count) {
        return _originalFilter.Read(buffer, offset, count);
    }

    public override long Seek(long offset, SeekOrigin origin) {
        return _originalFilter.Seek(offset, origin);
    }

    public override void SetLength(long value) {
        _originalFilter.SetLength(value);
    }
    public override void Close() {
        _originalFilter.Close();
    }

    public override void Write(byte[] buffer, int offset, int count) {
        byte[] data = new byte[count];
        Buffer.BlockCopy(buffer, offset, data, 0, count);
        string html = Encoding.Default.GetString(buffer);

        html = ExtractPageTopHiddenFields(html);
        html = InsertExtractedHiddenFields(html);

        byte[] outdata = Encoding.Default.GetBytes(html);
        _originalFilter.Write(outdata, 0, outdata.GetLength(0));
    }

    private string ExtractPageTopHiddenFields(string html) {
        int formStartIndex = html.IndexOf("<form");
        if(formStartIndex < 0) {
            return html;
        }

        int divStartOpenIndex = html.IndexOf("<div", formStartIndex);
        if(divStartOpenIndex < 0) {
            return html;
        }
        int divStartCloseIndex = html.IndexOf(">", divStartOpenIndex);
        int divStartLenght = divStartCloseIndex - divStartOpenIndex;
        int divEndOpenIndex = html.IndexOf("</div", divStartOpenIndex);
        int divEndCloseIndex = html.IndexOf(">", divEndOpenIndex);

        int divContentLength = divEndOpenIndex - divStartCloseIndex - 1;

        _topPageHiddenFields = html.Substring(divStartCloseIndex + 1, divContentLength);
        html = html.Remove(divStartOpenIndex, divEndCloseIndex - divStartOpenIndex + 1);
        return html;
    }

    private string InsertExtractedHiddenFields(string html) {
        if(string.IsNullOrEmpty(_topPageHiddenFields)) {
            return html;
        }

        int insertIndex = html.IndexOf("</form>");
        if(insertIndex > 0) {
            html = html.Insert(insertIndex, _topPageHiddenFields + Environment.NewLine);
            _topPageHiddenFields = null;
        }

        return html;
    }
}

One bonus is that it moves all available Hidden Input HTML-tags controlled by ASP.NET to the bottom of the Form-tag. It seems to skip the Hidden Input that handles EventValidation, which is a good thing, because ASP.NET seems to want this tag early in the Page Life Cycle.