Wednesday, November 12, 2008

Applications Can't Use SharePoint Master Pages

This is the story of stupid SharePoint problem and an ugly, kludgy, and embarrasing solution. On our project we have the need for an application that looks and feels like a SharePoint subsite. Specifically, we need it to inherit from SharePoint's masterpage. But we also need it to be a separate IIS application.

Problem: SharePoint dislikes Applications

We get the following error when we try to dynamically set the MasterPageFile in code:

System.ArgumentException: The virtual path '/_layouts/application.master' maps to another application, which is not allowed.

Several sites say we just can't do what we're trying to do. We considered a Virtual Path Provider, but decided not to even go down that path because SharePoint's master page undoubtedly has hooks into web.config and httpmodules and such, so we came up with the following hack:

Solution: An Ugly SharePoint Text Manipulation Hack

Deploy a nearly blank page in to /_layouts/ (we do this in our WebAppManifest.xml & WebAppSolution.ddf files). In PageBase (that all pages inherit from) we override Render to retrieve the blank page and do text manipulation to insert our content into it. Here's the code:

protected override void Render(HtmlTextWriter writer) {
  HttpWebRequest request = (HttpWebRequest)WebRequest.Create(GetUrlToBlankPage());
  request.Credentials = CredentialCache.DefaultCredentials;
  using (WebResponse webResponse = request.GetResponse()) {
    StreamReader streamReader = new StreamReader(webResponse.GetResponseStream());
    string pageHostHtml = streamReader.ReadToEnd();

    using (StringWriter stringWriter = new StringWriter())
    using (HtmlTextWriter htmlTextWriter = new HtmlTextWriter(stringWriter)) {
      base.Render(htmlTextWriter);
     
      string renderPageHtml = stringWriter.ToString();

      string title = GetTitleFromHtml(renderPageHtml);
      pageHostHtml.Replace("PAGE TITLE", title);

      string content = GetContentFromHtml(renderPageHtml);
     
      pageHostHtml = RemoveFormData(pageHostHtml);
      pageHostHtml = pageHostHtml.Replace("DO NOT MODIFY THIS PAGE", content);
    }
    writer.Write(pageHostHtml);
  }
}

private static string GetContentFromHtml(string html) {
  Match match = Regex.Match(html, "<body>(.*)</body>",
    RegexOptions
.IgnoreCase | RegexOptions.Singleline);
  return match.Success ? match.Groups[1].Captures[0].Value : "";
}

private static string GetTitleFromHtml(string html) {
  Match match = Regex.Match(html, "<title>(.*)</title>", RegexOptions.IgnoreCase);
  return match.Success ? match.Groups[1].Captures[0].Value : "";
}

private static string RemoveFormData(string html) {
  string replaced = Regex.Replace(html, "<form[^>]*>", "",,
    RegexOptions
.IgnoreCase | RegexOptions.Singleline);
  replaced = replaced.Replace("</form>", "");
  return replaced;
}

Conclusion

I’m sure there are plenty of optimizations that we can do (e.g. caching), but that’s the basic idea. But this solution makes me feel so dirty. So please, please, dear reader, tell me there is a better way. I want to feel clean again.

No comments: