Skip to content

Instantly share code, notes, and snippets.

@Matthew-Wise
Last active July 2, 2022 19:50
Show Gist options
  • Save Matthew-Wise/80626bbf9c9590228fc317774f15222a to your computer and use it in GitHub Desktop.
Save Matthew-Wise/80626bbf9c9590228fc317774f15222a to your computer and use it in GitHub Desktop.
Block Previews - using ViewComponents
/*
*
* This uses https://github.com/dawoe/24-days-block-list-article/blob/develop/24Days.Core/Controllers/BlockPreviewApiController.cs
* as a base to follow all I did was change it from Partial views to ViewComponents. Yet to check if I can register the ViewComponentHelper instead of the cast.
* You will also need the App_Plugins from Dave's repo.
*/
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using HtmlAgilityPack;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Web.BackOffice.Controllers;
using Umbraco.Extensions;
public class BlockPreviewApiController : UmbracoAuthorizedJsonController
{
private readonly IPublishedRouter _publishedRouter;
private readonly BlockEditorConverter _blockEditorConverter;
private readonly ILogger<BlockPreviewApiController> _logger;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly ITempDataProvider _tempDataProvider;
private readonly ITypeFinder _typeFinder;
private readonly IPublishedValueFallback _publishedValueFallback;
private readonly DefaultViewComponentHelper _viewComponentHelper;
public BlockPreviewApiController(IPublishedRouter publishedRouter,
BlockEditorConverter blockEditorConverter,
ILogger<BlockPreviewApiController> logger,
IUmbracoContextAccessor umbracoContextAccessor,
IVariationContextAccessor variationContextAccessor,
ITempDataProvider tempDataProvider,
ITypeFinder typeFinder,
IPublishedValueFallback publishedValueFallback,
IViewComponentHelper viewComponentHelper)
{
_publishedRouter = publishedRouter;
_blockEditorConverter = blockEditorConverter;
_logger = logger;
_umbracoContextAccessor = umbracoContextAccessor;
_variationContextAccessor = variationContextAccessor;
_tempDataProvider = tempDataProvider;
_typeFinder = typeFinder;
_publishedValueFallback = publishedValueFallback;
_viewComponentHelper = viewComponentHelper as DefaultViewComponentHelper ?? throw new ArgumentException($"Was not of type {nameof(DefaultViewComponentHelper)}", nameof(viewComponentHelper));
}
// <summary>
/// Renders a preview for a block using the associated razor view.
/// </summary>
/// <param name="data">The json data of the block.</param>
/// <param name="pageId">The current page id.</param>
/// <param name="culture">The culture</param>
/// <returns>The markup to render in the preview.</returns>
[HttpPost]
public async Task<IActionResult> PreviewMarkup([FromBody] BlockItemData data, [FromQuery] int pageId = 0, [FromQuery] string culture = "")
{
string markup;
try
{
IPublishedContent? page = null;
// If the page is new, then the ID will be zero
if (pageId > 0)
{
page = GetPublishedContentForPage(pageId);
}
if (page == null)
{
return Ok("The page is not saved yet, so we can't create a preview. Save the page first.");
}
await SetupPublishedRequest(culture, page);
markup = await GetMarkupForBlock(data);
}
catch (Exception ex)
{
markup = "Something went wrong rendering a preview.";
_logger.LogError(ex, "Error rendering preview for a block");
}
return Ok(CleanUpMarkup(markup));
}
private async Task<string> GetMarkupForBlock(BlockItemData blockData)
{
var element = _blockEditorConverter.ConvertToElement(blockData, PropertyCacheLevel.None, true);
var blockType = _typeFinder.FindClassesWithAttribute<PublishedModelAttribute>().FirstOrDefault(x =>
x.GetCustomAttribute<PublishedModelAttribute>(false)?.ContentTypeAlias == element?.ContentType.Alias);
if (blockType == null || element == null)
{
throw new Exception($"Unable to find BlockType {element?.ContentType.Alias}");
}
// create instance of the models builder type based from the element
var blockInstance = Activator.CreateInstance(blockType, element, _publishedValueFallback);
// get a generic block list item type based on the models builder type
var blockListItemType = typeof(BlockListItem<>).MakeGenericType(blockType);
// create instance of the block list item
// if you want to use settings this will need to be changed.
var blockListItem = (BlockListItem?)Activator.CreateInstance(blockListItemType, blockData.Udi, blockInstance, null, null);
var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary())
{
Model = blockListItem
};
await using var sw = new StringWriter();
var viewContext = new ViewContext(ControllerContext, new FakeView(), viewData, new TempDataDictionary(HttpContext, _tempDataProvider), sw, new HtmlHelperOptions());
_viewComponentHelper.Contextualize(viewContext);
var result = await _viewComponentHelper.InvokeAsync(element.ContentType.Alias.ToFirstUpper(), blockListItem);
result.WriteTo(sw, HtmlEncoder.Default);
return sw.ToString();
}
private async Task SetupPublishedRequest(string culture, IPublishedContent page)
{
// set the published request for the page we are editing in the back office
if (!_umbracoContextAccessor.TryGetUmbracoContext(out var context))
{
return;
}
// set the published request
var requestBuilder = await _publishedRouter.CreateRequestAsync(new Uri(Request.GetDisplayUrl()));
requestBuilder.SetPublishedContent(page);
context.PublishedRequest = requestBuilder.Build();
if (page.Cultures == null)
{
return;
}
// if in a culture variant setup also set the correct language.
var currentCulture = string.IsNullOrWhiteSpace(culture) ? page.GetCultureFromDomains() : culture;
if (currentCulture == null || !page.Cultures.ContainsKey(currentCulture))
{
return;
}
var cultureInfo = new CultureInfo(page.Cultures[currentCulture].Culture);
Thread.CurrentThread.CurrentCulture = cultureInfo;
Thread.CurrentThread.CurrentUICulture = cultureInfo;
_variationContextAccessor.VariationContext = new VariationContext(cultureInfo.Name);
}
private IPublishedContent? GetPublishedContentForPage(int pageId)
{
if (!_umbracoContextAccessor.TryGetUmbracoContext(out var context))
{
return null;
}
// Get page from published cache.
var page = context.Content?.GetById(pageId);
if (page == null)
{
// If unpublished, then get it from preview
page = context.Content?.GetById(true, pageId);
}
return page;
}
private static string CleanUpMarkup(string markup)
{
if (string.IsNullOrWhiteSpace(markup))
{
return markup;
}
var content = new HtmlDocument();
content.LoadHtml(markup);
// make sure links are not clickable in the back office, because this will prevent editing
var links = content.DocumentNode.SelectNodes("//a");
if (links != null)
{
foreach (var link in links)
{
link.SetAttributeValue("href", "javascript:;");
}
}
return content.DocumentNode.OuterHtml;
}
}
internal class FakeView : IView
{
public string Path => string.Empty;
public Task RenderAsync(ViewContext context)
{
return Task.CompletedTask;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment