1

I'm trying to create a HtmlHelper that is based upon the way in which Html.RenderAction works. The way in which it will differ from RenderAction is that along with the "ActionName" and "ControllerName", it is to take a string that in turn will correspond to a value in Web.Config. This value in the config is that of a URL.

The reason for this is that although my Controller/Action works perfectly in it's own native Project, I need to get the results of this Controller/Action from it's sibling Projects. I intend to do this by constructing the required URL using a helper and the details currently held in Web.Config of each of the sibling Projects.

I have the following coded already:

    public static void RenderActionToSpecifiedAssembly(this HtmlHelper helper, string actionName, string controllerName, string parentAssembly)
    {
        var uriFromWebConfig = new Uri(ConfigurationManager.AppSettings[parentAssembly]);
            //uriFromWebConfig == "http://ProjectNumberOne.com/"
        var parentUri = new Uri(uriFromWebConfig);
        var path = controllerName + "/" + actionName;
        var redirect = new Uri(parentUri, path).AbsoluteUri;
            //var redirect == "http://ProjectNumberOne.com/MyController/MyAction"
        //******************
    }

What I'm struggling with now is what to put at the **********'s. What I want this helper to do is return the result of http://ProjectNumberOne.com/MyController/MyAction.

  • If I type this URL into my address bar, it returns the expected html page.
  • If I use Html.RenderAction("MyAction","MyController") in the parent Project, it returns the expected html page.

What I don't know how to do is specify at the end of the helper the return URL for the sibling Projects to get the resulting html from.

Any ideas?

Alexei Levenkov
  • 98,904
  • 14
  • 127
  • 179
Stu1986C
  • 1,452
  • 1
  • 18
  • 34
  • Is your problem that `ConfigurationManager.AppSettings[parentAssembly]` returns empty instead of the value on web.config? How is this value assigned? I think you should set this variable like `parentASsembly = Assembly.GetEntryAssembly()`. Also, consider tagging this kind of question with Reflection – Davi Fiamenghi Nov 21 '14 at 16:17
  • `ConfigurationManager.AppSettings[parentAssembly]` returns as expected. I pass the string of "ProductOne" as the third parameter of the helper method. From there, the method itself looks in Web.Config to see what is held against the item called "ProductOne". (In this case it's a URL). My problem is when I'm in either Product2 or Product3 as I want it to get the results from http;//ProjectNumberOne.com/MyController/MyAction and not http;//ProjectNumberTwo.com/MyController/MyAction or http;//ProjectNumberThree.com/MyController/MyAction – Stu1986C Nov 21 '14 at 16:23
  • It's hard to tell exactly what you're going for but it seems to me that you're trying to fetch a configuration value from a different configuration per assembly. You might want to take a look at [this](http://stackoverflow.com/a/6151688/607701). – David Peden Nov 23 '14 at 16:24
  • I've changed title to what I think reflect your question - feel free to improve. You may want to remove most of the code from your question that is not directly related to actual problem (i.e. all config related code/text can be safely removed) – Alexei Levenkov Nov 30 '14 at 03:41

3 Answers3

0

Some of the details in your post make it difficult to determine whether you want to accomplish your goal by using reflection or by using http (or maybe you do not care as long as you can get the result you want).

I do not recommend that you try to utilize reflection to accomplish this. (there are a lot of reasons for this but the important thing is that using http is going to be easier and more straight-forward)

Based on what you've posted the approach I would use is to create an HtmlHelper that renders an IFRAME tag with the desired URL as the src.

Something like this (lots of opportunity for defensive coding here BTW):

    public static MvcHtmlString RenderIFrameForCompanionSite(this HtmlHelper helper, string actionName, string controllerName, string baseUrlSettingKey)
    {
        var baseUrlFromWebConfig = ConfigurationManager.AppSettings[baseUrlSettingKey];
        var companionSiteUri = new Uri(baseUrlFromWebConfig);
        var path = controllerName + "/" + actionName;
        var redirect = new Uri(companionSiteUri, path).AbsoluteUri;

        return new MvcHtmlString("<iframe style='width: 100%' src='" + redirect + "'></iframe>");
    }

A view in the ProjectTwo site would reference an action in ProjectOne like this:

@Html.RenderIFrameForCompanionSite("MyAction", "MyController", "ProjectOne")

I'm assuming that your application(s) have a business need to be in distinct sites/projects. But if that is not mandatory then you might consider whether it would be possible to organize the related content differently (such as a single site with multiple areas rather than multiple sites) which would make it easier for you to share the content/behaviors between them.

David Tansey
  • 5,813
  • 4
  • 35
  • 51
  • I agree that reflection is not the way in which I'd try to accomplish the outcome of this problem. It looks like you've understood my problem though :) I'll have to try the IFrame when I get to work in the morning. Yeh, unfortunately the 4 separate products have been designed, coded and released already; I've just started in my current role and have the job of doing a lot of refactoring. The ideal solution for this particular problem would be more of a SPA approach but time and cost is against me. I'll let you know how I get on. Thanks – Stu1986C Nov 23 '14 at 19:40
0

I found that although the IFRAME approach offered by David worked as required, using an IFRAME still unsettled me. This post made me realise that they're not so great: Good Reasons why not to use Iframes in page content

I found that the following implementation served it's purpose and gave me the required result:

public static IHtmlString RenderActionToSpecifiedAssembly(this HtmlHelper helper, string actionName, string controllerName, string parentAssembly)
{
        var parentWebConfigarentValue = new Uri(ConfigurationManager.AppSettings[parentAssembly]);
        var path = controllerName + "/" + actionName;
        var redirect = new Uri(parentWebConfigarentValue, path).AbsoluteUri;
        var request = (HttpWebRequest)WebRequest.Create(redirect);

        var result = (HttpWebResponse)request.GetResponse();

        String responseString;

        using (Stream stream = result.GetResponseStream())
        {
            StreamReader reader = new StreamReader(stream, Encoding.UTF8);
            responseString = reader.ReadToEnd();
        }

        return new HtmlString(responseString);
}

Using this approach allows me to make a request to the Parent Controller/Action, write it to string then return the HTML to the page to be rendered. Works a charm! :)

Community
  • 1
  • 1
Stu1986C
  • 1,452
  • 1
  • 18
  • 34
  • Just to make comment, obviously alot of the functionality can be refactored into individual methods that encapsulate their own concerns but for the purpose of putting on the internet, it's easier to lay it all out in one method. – Stu1986C Nov 25 '14 at 12:29
  • Side note: running non-trivial code (like query DB or proxy page from remote server) is generally not recommended as part of view logic. It also (before ASP.Net MVC v6 comes out) prevents you from making this code `async` (as views/child actions don't support async processing in ASP.Net MVC 5 and below). – Alexei Levenkov Nov 30 '14 at 03:35
  • Side note 2: obviously this will produce valid page only when external server renders partial result on that url (which likely the case for OP, but deserves warning - while most browsers will render pages with nested HTML/BODY tags somewhat reasonably it is not a good practice to render invalid HTML). – Alexei Levenkov Nov 30 '14 at 03:38
0

Your solution better explains what you want, but is very inflexible. What if you need to pass additional route values to the request? It would be better to mimic the UrlHelper class by providing overloads that you can use to build any URL you want.

using System;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

public static class HtmlHelperExtensions
{
    public static IHtmlString RenderActionToSpecifiedAssembly(
        this HtmlHelper helper, string actionName, string parentAssembly)
    {
        return RenderActionToSpecifiedAssembly(
            helper, actionName, null, null, null, parentAssembly);
    }

    public static IHtmlString RenderActionToSpecifiedAssembly(
        this HtmlHelper helper, string actionName, object routeValues, 
        string parentAssembly)
    {
        return RenderActionToSpecifiedAssembly(helper, actionName, 
            null, new RouteValueDictionary(routeValues), null, parentAssembly);
    }

    public static IHtmlString RenderActionToSpecifiedAssembly(
        this HtmlHelper helper, string actionName, string controllerName, 
        string parentAssembly)
    {
        return RenderActionToSpecifiedAssembly(helper, actionName, 
            controllerName, null, null, parentAssembly);
    }

    public static IHtmlString RenderActionToSpecifiedAssembly(
        this HtmlHelper helper, string actionName, RouteValueDictionary routeValues, 
        string parentAssembly)
    {
        return RenderActionToSpecifiedAssembly(helper, actionName, 
            null, routeValues, null, parentAssembly);
    }

    public static IHtmlString RenderActionToSpecifiedAssembly(
        this HtmlHelper helper, string actionName, string controllerName, 
        object routeValues, string parentAssembly)
    {
        return RenderActionToSpecifiedAssembly(helper, actionName, 
            controllerName, new RouteValueDictionary(routeValues), 
            null, parentAssembly);
    }

    public static IHtmlString RenderActionToSpecifiedAssembly(
        this HtmlHelper helper, string actionName, string controllerName, 
        RouteValueDictionary routeValues, string parentAssembly)
    {
        return RenderActionToSpecifiedAssembly(helper, actionName, 
            controllerName, routeValues, parentAssembly, null);
    }

    public static IHtmlString RenderActionToSpecifiedAssembly(
        this HtmlHelper helper, string actionName, string controllerName, 
        object routeValues, string protocol, string parentAssembly)
    {
        return RenderActionToSpecifiedAssembly(helper, actionName, 
            controllerName, routeValues, protocol, parentAssembly, null);
    }

    public static IHtmlString RenderActionToSpecifiedAssembly(
        this HtmlHelper helper, string actionName, string controllerName, 
        RouteValueDictionary routeValues, string parentAssembly, string port)
    {
        var hostName = ConfigurationManager.AppSettings[parentAssembly];
        var url = GenerateContentUrl(helper, actionName, 
            controllerName, routeValues, null, hostName, port);
        return RenderContents(url);
    }

    public static IHtmlString RenderActionToSpecifiedAssembly(
        this HtmlHelper helper, string actionName, string controllerName, 
        object routeValues, string protocol, string parentAssembly, string port)
    {
        var hostName = ConfigurationManager.AppSettings[parentAssembly];
        var url = GenerateContentUrl(helper, actionName, 
            controllerName, new RouteValueDictionary(routeValues), 
            protocol, hostName, port);
        return RenderContents(url);
    }

    private static string GenerateContentUrl(this HtmlHelper helper, 
        string actionName, string controllerName, RouteValueDictionary routeValues, 
        string protocol, string hostName, string port)
    {
        var currentUri = helper.ViewContext.RequestContext.HttpContext.Request.Url;

        // Ensure we have an absolute path
        if (string.IsNullOrEmpty(protocol) && string.IsNullOrEmpty(hostName))
        {
            // Match the scheme of the current request so we don't get a
            // security warning in the browser.
            protocol = currentUri.Scheme;
        }

        // Allow caller to override the port so it doesn't have 
        // to be the same as the current request.
        string currentUrl = currentUri.Scheme + Uri.SchemeDelimiter 
            + currentUri.DnsSafeHost;
        if (!string.IsNullOrEmpty(port))
        {
            currentUrl += ":" + port;
        }
        currentUrl += "/";
        var homePageUri = new Uri(new Uri(currentUrl, UriKind.Absolute), "/");

        // Create a TextWriter with null stream as a backing stream 
        // which doesn't consume resources
        using (var nullWriter = new StreamWriter(Stream.Null))
        {
            // Create a fake context at the home page to ensure that ambient values  
            // from the request are excluded from the generated URL.
            // See: https://aspnetwebstack.codeplex.com/workitem/1346
            var httpContext = CreateHttpContext(homePageUri, nullWriter);
            var requestContext = new RequestContext(httpContext, new RouteData());
            return UrlHelper.GenerateUrl(null, actionName, controllerName, 
                protocol, hostName, null, routeValues, helper.RouteCollection, 
                requestContext, true);
        }
    }

    private static HttpContextBase CreateHttpContext(Uri uri, TextWriter writer)
    {
        if (uri == null)
            throw new ArgumentNullException("uri");
        if (writer == null)
            throw new ArgumentNullException("writer");

        var request = new HttpRequest(string.Empty, uri.ToString(), uri.Query);
        var response = new HttpResponse(writer);
        var httpContext = new HttpContext(request, response);
        return new HttpContextWrapper(httpContext);
    }

    private static IHtmlString RenderContents(string url)
    {
        var request = (HttpWebRequest)WebRequest.Create(url);
        var result = (HttpWebResponse)request.GetResponse();

        String responseString;
        using (Stream stream = result.GetResponseStream())
        {
            StreamReader reader = new StreamReader(stream, Encoding.UTF8);
            responseString = reader.ReadToEnd();
        }

        return new HtmlString(responseString);
    }
}

You may also wish to parse the port out of the configuration value so you can have both http and https URLs configured there and so they are tightly bound together with the host name, which could remove a few overloads. In that case, you should probably use the protocol of the current request to determine which (HTTP or HTTPS) configuration value to retrieve and remove protocol as a passed in value.

NightOwl888
  • 55,572
  • 24
  • 139
  • 212