Last active
July 2, 2022 19:50
-
-
Save Matthew-Wise/80626bbf9c9590228fc317774f15222a to your computer and use it in GitHub Desktop.
Block Previews - using ViewComponents
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* | |
* 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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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