5

I have two renderings. Let's call them Form Block and Complete Form.

Complete Form has a placeholder on it that can contain Form Blocks. Form Blocks have a placeholder that can contain other components that make up the form.

But here's the complicated part

I want content editors to add components to Form Blocks using the Experience Editor, directly on the Form Block (in other words, not on the page that contains the Form Block). For this reason, there are presentation details set on the Form Block.

When I render the Complete Form (with its list of Form Blocks), I want to get the rendered output of the Form Block using its presentation details, not the presentation details set on the Complete Form for it.

Let me try to explain with some code

When we render a Complete Form, one or more Form Blocks may get rendered. For each one, the FormBlock controller action gets called:

// FormBlock controller rendering action
public ActionResult FormBlock()
{
    var dataSource = this.GetDataSourceItem<IFormBlock>();

    var dataSourceItem = SitecoreContext.ResolveItem(dataSource);
    var currentDevice = Sitecore.Context.Device;
    var renderings = dataSourceItem.Visualization.GetRenderings(currentDevice, false);

    var stringBuilder = new StringBuilder();
    foreach (var rendering in renderings)
    {
        // TODO: Render the rendering
    }

    return View("RawOutput", stringBuilder.ToString());
}
6

I solved this in two ways. Depending on your needs, one may be more suitable.

The Simpler Way

As was pointed out to me by Mark Cassidy's comment, replacing the standard Item Rendering behavior would meet the requirement, and not require a controller rendering or any of my original code. See the solution here:

https://www.cmsbestpractices.com/sitecore-item-rendering-best-practice/

For More Customization

The disadvantage of the first approach is that it exposes the Experience Editor features of all the controls on the Form Block, too, which we don't want; we want to prevent people from editing Form Blocks when they've been added to another page. To solve this issue, I updated my controller method as follows. The pertinent code was found by decompiling Sitecore DLLs.

// FormBlock controller rendering action
public ActionResult FormBlock()
{
    var dataSource = this.GetDataSourceItem<IFormBlock>();

    var dataSourceItem = SitecoreContext.ResolveItem(dataSource);
    var currentDevice = Sitecore.Context.Device;

    XmlNode deviceNode = new LayoutField(dataSourceItem).GetDeviceNode(currentDevice);

    var stringBuilder = new StringBuilder();
    if (deviceNode != null && deviceNode.HasChildNodes)
    {
        SiteContext siteContext = SiteContext.GetSite(Sitecore.Context.Site.Name);
        siteContext.SetDisplayMode(DisplayMode.Normal, DisplayModeDuration.Temporary);
        using (new SiteContextSwitcher(siteContext))
        {
            var parser = new XmlBasedRenderingParser();
            foreach (XmlNode renderingNode in deviceNode.ChildNodes)
            {
                Sitecore.Mvc.Presentation.Rendering rendering = parser.Parse(XElement.Parse(renderingNode.OuterXml), false);

                var renderingArgs = new RenderRenderingArgs(rendering, new StringWriter(stringBuilder));

                CorePipeline.Run("mvc.renderRendering", renderingArgs);
            }
        }
    }

    return Content(stringBuilder.ToString());
}
5

Sitecore MVC have 2 types of renderings: View Rendering, Controller Action Rendering. To be simple - controller action is only some logic that fill data to model and transfer it to view.

You can use Razor view engine to render rendering view:

public string RenderRazorViewToString(string viewName, object model)
{
  ViewData.Model = model;
  using (var sw = new StringWriter())
  {
    var viewResult = ViewEngines.Engines.FindPartialView(ControllerContext,
                                                             viewName);
    var viewContext = new ViewContext(ControllerContext, viewResult.View,
                                 ViewData, TempData, sw);
    viewResult.View.Render(viewContext, sw);
    viewResult.ViewEngine.ReleaseView(ControllerContext, viewResult.View);
    return sw.GetStringBuilder().ToString();
  }
}

To render Controller Action Rendering you can use same code, but return model that is filled by your logic.

If you need to use Sitecore rendering view engine wrapper and render View Rendering(or Controller Action Rendering) go through all Sitecore processors (e.g. GlassMapper get model, render other renderings in placeholders) - it is much more complex. You can take a look on this example. It was done to render one rendering and transfer it via AJAX and don't lose personalization, rendering parameters, datasource, nested renderings in placeholders:

if (rendering != null)
{
    args.PageDefinition.Renderings.Remove(rendering);
    //Put this rendering into ajax layout  
    rendering.Placeholder = Constants.Strings.AjaxPlaceholderKey;

    rendering.LayoutId = new Guid(Constants.IDs.AjaxEmptyLayout);
    var layout = renderings.First(x => x.RenderingType == "Layout");
    if (layout != null)
    {
      args.PageDefinition.Renderings.Remove(layout);
      for ( int i=0; i< args.PageDefinition.Renderings.Count; i++)
      {
        args.PageDefinition.Renderings[i].Placeholder =
          args.PageDefinition.Renderings[i].Placeholder.Split('/').Last();
        args.PageDefinition.Renderings[i].LayoutId =
          new Guid(Constants.IDs.AjaxEmptyLayout);
      }
      layout.LayoutId = new Guid(Constants.IDs.AjaxEmptyLayout);
      var getRedererArgs = new GetRendererArgs(new Rendering());
      getRedererArgs.LayoutItem =
        Sitecore.Context.Database.GetItem(Constants.IDs.AjaxEmptyLayout);

      if (rendering.Renderer is Sitecore.Mvc.Presentation.ControllerRenderer)
      {
        RouteValueDictionary routeValueDictionary = new RouteValueDictionary();
        foreach (var key in  HttpContext.Current.Request.QueryString.AllKeys.Select(x=> x.ToString()))
        {
          routeValueDictionary.Add(key, HttpContext.Current.Request.QueryString[key]);
        }
        ((Presentation.ControllerRenderer)rendering.Renderer).routeValueDictionary = routeValueDictionary;
      }
      layout.Renderer = PipelineService.Get().RunPipeline<GetRendererArgs, Renderer>(PipelineNames.GetRenderer, getRedererArgs, a => a.Result);

      args.PageDefinition.Renderings.Add(layout);
      args.PageDefinition.Renderings.Add(rendering);
    }
}

You can adopt this code to be used not inside ActionResult, but not via Sitecore processor.

Also, I think that you can take a look on Sitecore FxM implementation as there is used same approach (code via ILSpy reflector):

public string GetRenderingHtml(Guid id, string uniqueId, string placeholder, string dataSource = "")
{
    RenderingItem item = RenderingItem.GetItem(new ID(id), this.sitecoreContext.Database.Database, false);
    if (item == null)
    {
        return string.Empty;
    }
    if (string.IsNullOrEmpty(placeholder))
    {
        return string.Empty;
    }
    if (string.IsNullOrEmpty(uniqueId))
    {
        return string.Empty;
    }
    string[] array = placeholder.TrimStart(new char[]
    {
        '/'
    }).Split(new char[]
    {
        '/'
    });
    if (array.Length <= 0)
    {
        return string.Empty;
    }
    string text = array[0];
    if (this.sitecoreContext.Database.GetItem(text) == null)
    {
        return string.Empty;
    }
    string result;
    try
    {
        if (item.IsMvcRendering())
        {
            result = this.GenerateRenderingHtml(id, uniqueId, placeholder, text, dataSource);
        }
        else
        {
            result = this.GenerateSublayoutHtml(id, uniqueId, placeholder, text, dataSource);
        }
    }
    catch (ItemNotFoundException ex)
    {
        throw this.HandleError(ex);
    }
    catch (Exception ex2)
    {
        throw this.HandleError(ex2);
    }
    return result;
}

Conclusion, there are two ways:

  1. Make your views simpler(render from model, don't use a lot of Sitecore extensions) to have ability to use Razor rendering view engine.
  2. Write your own code, you have as examples 2 implementations(GitHub or Sitecore Federated Experience Manager). Your solution will be complex, but will be suitable almost for every rendering.
  • Your second option could work for us, but it uses the context item to get the presentation details instead of the datasource of the selected rendering. Is there a way to construct the necessary args object for a non-context item? – Dan Sinclair Dec 28 '16 at 21:38
  • @DanSinclair you can easily construct args for pipeline renderLayout and call it (via CorePipeline.Run), but better take a look on RenderRendering. It takes args.Rendering as parameter which contains Datasource property and it contains TextWriter that could be used to get rendered string. – Anton Dec 28 '16 at 21:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.