Part of the solution is to use a custom API to return the populated HTML of each block by constructing a typed object from its JSON data. The pertinent code which does this is this method:
private async Task<string> GetMarkupForBlock(BlockItemData blockData)
{
// convert the json data to a IPublishedElement (using the built-in conversion)
var element = this._blockEditorConverter.ConvertToElement(blockData, PropertyCacheLevel.None, true);
// get the models builder type based on content type alias
var blockType = _typeFinder.FindClassesWithAttribute<PublishedModelAttribute>().FirstOrDefault(x =>
x.GetCustomAttribute<PublishedModelAttribute>(false).ContentTypeAlias == 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);
// render the partial view for the block.
var partialName = $"/Views/Partials/blocklist/Components/{element.ContentType.Alias}.cshtml";
var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary());
viewData.Model = blockListItem;
var actionContext = new ActionContext(this.HttpContext, new RouteData(), new ActionDescriptor());
await using var sw = new StringWriter();
var viewResult = _razorViewEngine.GetView(partialName, partialName, false);
if (viewResult?.View != null)
{
var viewContext = new ViewContext(actionContext, viewResult.View, viewData, new TempDataDictionary(actionContext.HttpContext, _tempDataProvider), sw, new HtmlHelperOptions());
await viewResult.View.RenderAsync(viewContext);
}
return sw.ToString();
}
This all works great. However, the model is returned without any Settings data. I wondered if anyone had any pointers as to how to append the settings data to the model too as I use that data to do some important stuff in the view. A lot of this stuff is over my head!
The settings will be available in your view that this code renders, so it will display what your block looks like in the UI, clicking the view in the backoffice should still give you access to the settings.
Thanks Huw. I'm making progress, but hit a bit of a stopper.
To me, it doesn't seem possible to pull all of the settings data together from only the 'data' object that's posted from Angular to the API. A BlockListItem is obviously composed of two doc-types, one for content and one for settings, so it would seem that both need to be passed to the API in order to reconstruct a 'fully hydrated' instance of a block (could be wrong but could not see how to get settings from only the content part). So what I've done is modified the Angular controller and resource such that they're now posting an object to the API which contains both content and settings data for the block - it just posts them both as part of a wrapped json object:
Both content and settings objects are then passed as parameters into the GetMarkupForBlock method in the API controller. This works. Stepping through it I can see both parts populated correctly.
It's then possible in the GetMarkupForBlock method to effectively duplicate all of the existing code to construct a settings version of all of the objects that are constructed. My code is slightly modified from the original but the updated version so far is:
private async Task<string> GetMarkupForBlock(BlockItemData blockContent, BlockItemData blockSettings)
{
// convert the json data to a IPublishedElement (using the built-in conversion)
var element = this._blockEditorConverter.ConvertToElement(blockContent, PropertyCacheLevel.None, true);
var settingsElement = this._blockEditorConverter.ConvertToElement(blockSettings, PropertyCacheLevel.None, true);
// get the models builder type based on content type alias
var blockType = _typeFinder.FindClassesWithAttribute<PublishedModelAttribute>().FirstOrDefault(x =>
x.GetCustomAttribute<PublishedModelAttribute>(false).ContentTypeAlias == element.ContentType.Alias);
var settingsBlockType = _typeFinder.FindClassesWithAttribute<PublishedModelAttribute>().FirstOrDefault(x =>
x.GetCustomAttribute<PublishedModelAttribute>(false).ContentTypeAlias == settingsElement.ContentType.Alias);
// create instance of the models builder type based from the element
var blockInstance = Activator.CreateInstance(blockType, element, _publishedValueFallback);
var settingsBlockInstance = Activator.CreateInstance(settingsBlockType, settingsElement, _publishedValueFallback);
// get a generic block list item type based on the models builder type
var blockListItemType = typeof(BlockListItem<>).MakeGenericType(blockType);
var settingsBlockListItemType = typeof(BlockListItem<>).MakeGenericType(settingsBlockType);
// 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, blockContent.Udi, blockInstance, null, null);
// render the partial view for the block.
var partialName = $"/Views/Partials/blocklist/Modules/{element.ContentType.Alias}.cshtml";
...etc
}
So I've now got both content and settings versions of all of the different parts.
The bit I'm struggling with is how then to bundle the content and settings things together into the Activator.CreateInstance method in order to construct the full BlockListItem which includes the settings. The signature on that method seems a little spurious to me - I'm not understanding how it works in order to know what to pass to it.
Actually I can see what Activator.CreateInstance is doing now. Passing blockSettings.Udi and settingsBlockInstance to it as the last 2 parameters (rather than null and null) seems to work, but something is still awry as the settings object is still null once it hits the view. Will try not to make this a running commentary but will keep going!
Figured it out. In the angular controller I was posting block.settings rather than block.settingsData. Sorry this is a bit piecemeal but as I say I've made quite a few modifications to the code along the way (added web components and a scaling directive to ensure desktop breakpoints are observed). The pertinent change in the block preview Angular controller is this:
...where $scope.watch has been modified to $scope.watchGroup to watch both data and settingsData. The loadPreview function has then been updated to post both things to the getPreview method of the preview resource:
Im not really a .net developer, but would love to have preview of block list items. But i'm having a bit of trouble getting Paul Seals tutorial to run. So i would like to try your approach, but i'm getting errors, so the full files would help a bunch.
Hey Claus. I've modified the files quite a lot for my implementation, and my version is really only a proof-of-concept so there are a few hacky things in there that I wouldn't deem 'production ready' but I can try to pull something generic out this evening if Huw's links don't land. Is it the settings part you're getting stuck on specifically or just getting the initial previews working?
I've implemented all of the code as recommended in this thread. But now a new blocklist item will only appear in the list if I save or publish. I set the code back to not include settings the item will render immediately.
Ok, I have worked out what the cause is, just need to figure out how to fix it :)
basically the original code uses $watch which whenever you add a new control it fires the preview api function. The new code is using watchgroup so that it can also get the settings data, however this does not seem to call the preview api when you add a new item, trying to figure out why that is.
I've not been able to check, but assume that the version I have is also susceptible to the same issue. Not sure if you saw this: https://github.com/dawoe/24-days-block-list-article/issues/2 but it applies the settings a slightly different way (using separate Watches rather than a watch group). Might be worth updating to see if that resolves the issue.
I will take a look, however I have managed to get mine working using watchCollection on the block, that now fires the preview controller, so definitely looks like watchGroup is broken.
Awwwwwesssome! That did the trick! Thank you so much! :-)
A quick question, I might be a bit of numpty on this one. I cannot get the angular controller to work without allowing language to pass as a null ?
I will also use this when I get around to trying out the new block grid editor in 10.3. Quite why this isn't baked into the codebase, I find a little crazy! I mean who really wants to have to write extra angular.js views when you can re-use what you already have!
Hey last question and i'll stop bugging you! Paul Seals demos on YouTube offer the ability to remove blocks from flow using a Hide toggle switch. The twenty four days code has a filter to set the item with less opacity to visually show its "disabled". This was working in the backoffice without the view taking the item completely out of the flow, but now it's removing it! One step forward.....lol
The code in the view is simply...
if (blockSettings?.Hide ?? false) { return; }
And in the directive...
function setStyle(scope, element) {
if (scope.settings.hide === '1') {
element[0].style.opacity = 0.25;
return;
}
It's pretty clear the return is preventing the view from rendering, so i'm a little ensure as to how it could've work before...but it did!
(EDIT: It worked before because Pauls code omitted settings - which I now have! lol. My first idea is I could use CSS class names and then use the CleanMarkUp method to strip the "hide" classname from the backoffice version. The downside is i'm passing HTML to the front I don't need.
I suppose I may need to come up with something that differentiates between the front and back, but that kinda defeats the point of re-use.
I may just take the "hide" option out altogether. Incidentally, the nested contently package by Nathan Wolfe does the same thing by providing a UI toggle and keying on umbracoNaviHide and IsVisible().
I never actually implemented hide stuff so not sure. having a nightmare today, windows update has killed IISExpress :( so trying to sort that out currently :D
Yea i'm probably overthinking the use case for content editors. We didn't have the function in nested content on our Umbraco 7 project and nobody ever complained!
I solved the problem pretty quickly by removing "hide" from the block model in the API controller.
if (blockSettings != null)
{
blockSettings.RawPropertyValues.Remove("hide");
...
I've really struggled getting a good dev. env. working in Umbraco 10. Previously I used a taskrunner in Umbraco 7 which combined:
Browsersync for CSS/JS code injection
Bundling/Minifying
SASS/BEM compiling
This made writing front end code a doddle.
Morroring this in Umbraco 10 while still being able to use dotnet watch run for all the CS/CSTML side of things took me flipping ages to iron out. The big issue for me was getting a local certificate working in Browsersync to proxy the kestrel server. I got there in the end and can now inject my SASS/CSS changes as well as getting recompilation of my views/cs.
This final step of getting a decent preview of my block modules will completely transform the way content editors use Umbraco. I've done a couple of demos and have had great feedback.
seems it isn't IISExpress, it has broken dotnet 6 too :( currently even dotnet run crashes, thanks Microsoft!! older .net frameworks run in IISExpress no problem. Guess I won't be doing much coding today :D :D
Append settings data to block list preview model generated via custom API
Hi,
I'm implementing razor previews for block list editor back-office previews, using this article and accompanying repository as a reference.
Part of the solution is to use a custom API to return the populated HTML of each block by constructing a typed object from its JSON data. The pertinent code which does this is this method:
This all works great. However, the model is returned without any Settings data. I wondered if anyone had any pointers as to how to append the settings data to the model too as I use that data to do some important stuff in the view. A lot of this stuff is over my head!
Many thanks for any help.
what exactly do you need to do?
The settings will be available in your view that this code renders, so it will display what your block looks like in the UI, clicking the view in the backoffice should still give you access to the settings.
Ah, I see what you mean, the preview gets a null settings :( I will take a look see if I can figure it out :)
Thanks Huw. I'm making progress, but hit a bit of a stopper.
To me, it doesn't seem possible to pull all of the settings data together from only the 'data' object that's posted from Angular to the API. A BlockListItem is obviously composed of two doc-types, one for content and one for settings, so it would seem that both need to be passed to the API in order to reconstruct a 'fully hydrated' instance of a block (could be wrong but could not see how to get settings from only the content part). So what I've done is modified the Angular controller and resource such that they're now posting an object to the API which contains both content and settings data for the block - it just posts them both as part of a wrapped json object:
Then in the API this new 'dual' object is split out into content and settings parts, each cast to a
BlockItemData
type:Both content and settings objects are then passed as parameters into the
GetMarkupForBlock
method in the API controller. This works. Stepping through it I can see both parts populated correctly.It's then possible in the
GetMarkupForBlock
method to effectively duplicate all of the existing code to construct a settings version of all of the objects that are constructed. My code is slightly modified from the original but the updated version so far is:So I've now got both content and settings versions of all of the different parts.
The bit I'm struggling with is how then to bundle the content and settings things together into the
Activator.CreateInstance
method in order to construct the full BlockListItem which includes the settings. The signature on that method seems a little spurious to me - I'm not understanding how it works in order to know what to pass to it.Any ideas on this last part would be amazing!
Actually I can see what
Activator.CreateInstance
is doing now. PassingblockSettings.Udi
andsettingsBlockInstance
to it as the last 2 parameters (rather thannull
andnull
) seems to work, but something is still awry as the settings object is still null once it hits the view. Will try not to make this a running commentary but will keep going!Yes, that is the part I was looking at too :)
Figured it out. In the angular controller I was posting
block.settings
rather thanblock.settingsData
. Sorry this is a bit piecemeal but as I say I've made quite a few modifications to the code along the way (added web components and a scaling directive to ensure desktop breakpoints are observed). The pertinent change in the block preview Angular controller is this:...where
$scope.watch
has been modified to$scope.watchGroup
to watch both data and settingsData. TheloadPreview
function has then been updated to post both things to thegetPreview
method of the preview resource:Worked great :)
Well done, I will try this out myself tomorrow :)
@Dan
Could you possibly share your files?
Im not really a .net developer, but would love to have preview of block list items. But i'm having a bit of trouble getting Paul Seals tutorial to run. So i would like to try your approach, but i'm getting errors, so the full files would help a bunch.
@Claushingebjerg
angular controller
C# controller
THANKS, but the c# controller link returns a 404 ;)
Hey Claus. I've modified the files quite a lot for my implementation, and my version is really only a proof-of-concept so there are a few hacky things in there that I wouldn't deem 'production ready' but I can try to pull something generic out this evening if Huw's links don't land. Is it the settings part you're getting stuck on specifically or just getting the initial previews working?
ah, sorry most likely because I forgot to change the file extension from .cs :)
Try this one
Uggh sooooo close!
I've implemented all of the code as recommended in this thread. But now a new blocklist item will only appear in the list if I save or publish. I set the code back to not include settings the item will render immediately.
Any ideas?
sorry no, will need to investigate
Thanks Huw
Would you like me to paste my code? To be fair it's just a merge of your examples and the original 24days demo.
Martin.
I will check how mine behaves first and if it is different then yes we can do a compare, but will check mine first
Huw you are a star!
Ok, I have worked out what the cause is, just need to figure out how to fix it :)
basically the original code uses $watch which whenever you add a new control it fires the preview api function. The new code is using watchgroup so that it can also get the settings data, however this does not seem to call the preview api when you add a new item, trying to figure out why that is.
I've not been able to check, but assume that the version I have is also susceptible to the same issue. Not sure if you saw this: https://github.com/dawoe/24-days-block-list-article/issues/2 but it applies the settings a slightly different way (using separate Watches rather than a watch group). Might be worth updating to see if that resolves the issue.
I will take a look, however I have managed to get mine working using watchCollection on the block, that now fires the preview controller, so definitely looks like watchGroup is broken.
I will post my code when I'm done testing it
ok, these are my js files, the apicontroller cs file is still the same as the one in the link I posted earlier.
preview.resource.js
block-preview.controller.js
Hi Huw
Awwwwwesssome! That did the trick! Thank you so much! :-)
A quick question, I might be a bit of numpty on this one. I cannot get the angular controller to work without allowing language to pass as a null ?
I will also use this when I get around to trying out the new block grid editor in 10.3. Quite why this isn't baked into the codebase, I find a little crazy! I mean who really wants to have to write extra angular.js views when you can re-use what you already have!
Can I buy you a coffee?
Thanks again Martin
Not sure TBH, seems OK for me, do you have multiple languages defined or just a single language?
Yes just en-gb so it may be that. It's no biggie.
Hey last question and i'll stop bugging you! Paul Seals demos on YouTube offer the ability to remove blocks from flow using a Hide toggle switch. The twenty four days code has a filter to set the item with less opacity to visually show its "disabled". This was working in the backoffice without the view taking the item completely out of the flow, but now it's removing it! One step forward.....lol
The code in the view is simply...
And in the directive...
It's pretty clear the return is preventing the view from rendering, so i'm a little ensure as to how it could've work before...but it did!
(EDIT: It worked before because Pauls code omitted settings - which I now have! lol. My first idea is I could use CSS class names and then use the CleanMarkUp method to strip the "hide" classname from the backoffice version. The downside is i'm passing HTML to the front I don't need.
I suppose I may need to come up with something that differentiates between the front and back, but that kinda defeats the point of re-use.
I may just take the "hide" option out altogether. Incidentally, the nested contently package by Nathan Wolfe does the same thing by providing a UI toggle and keying on umbracoNaviHide and IsVisible().
Any thoughts?
I never actually implemented hide stuff so not sure. having a nightmare today, windows update has killed IISExpress :( so trying to sort that out currently :D
Uggh! Sorry to hear!
Yea i'm probably overthinking the use case for content editors. We didn't have the function in nested content on our Umbraco 7 project and nobody ever complained!
Thanks again for your help.
M.
Hey Huw.
I hope you've solved your IISExpress issue!
I solved the problem pretty quickly by removing "hide" from the block model in the API controller.
I've really struggled getting a good dev. env. working in Umbraco 10. Previously I used a taskrunner in Umbraco 7 which combined:
This made writing front end code a doddle.
Morroring this in Umbraco 10 while still being able to use dotnet watch run for all the CS/CSTML side of things took me flipping ages to iron out. The big issue for me was getting a local certificate working in Browsersync to proxy the kestrel server. I got there in the end and can now inject my SASS/CSS changes as well as getting recompilation of my views/cs.
This final step of getting a decent preview of my block modules will completely transform the way content editors use Umbraco. I've done a couple of demos and have had great feedback.
All the best Martin
seems it isn't IISExpress, it has broken dotnet 6 too :( currently even dotnet run crashes, thanks Microsoft!! older .net frameworks run in IISExpress no problem. Guess I won't be doing much coding today :D :D
is working on a reply...