Unifying BlockListItem and BlockGridItem
# help-with-umbraco
s
How would you unify a partial used for both BlockListItem and BlockGridItem ? Link: https://our.umbraco.com/forum/using-umbraco-and-getting-started/114577-unifying-blocklistitem-and-blockgriditem
s
IIRC correctly, the lovely people at Bump does this. I think the gist of it, is that they simply just send the content part of the block item to the partial, and adds settings to the view data dictionary. Ie, from blockgrid/items.cshtml:
Copy code
diff
            data-row-span="@item.RowSpan"
            style=" --umb-block-grid--item-column-span: @item.ColumnSpan; --umb-block-grid--item-row-span: @item.RowSpan; ">
            @{
-               var partialViewName = "blockgrid/Components/" + item.Content.ContentType.Alias;
+               var partialViewName = "blocks/Components/" + item.Content.ContentType.Alias;
                try
                {
-                   @await Html.PartialAsync(partialViewName, item)
+                   @await Html.PartialAsync(partialViewName, item.Content, new ViewDataDictionary(ViewData){ { "settings", item.Settings }});
                }
                catch (InvalidOperationException)
                {
And then the partial view is like this (note it now inherits just the content model, and then settings (if needed) is fetched from the viewdata dictionary.
Copy code
cshtml
@inherits UmbracoViewPage<ContentModels.Accordion>
@{
    var settings = ViewData["settings"] as ContentModels.AccordionSetting;
}
d
I have thought about this in the past and seem to remember you can customise the paths to your partials / element types so I see no reason why you could not keep all of your components in a unified directory by modifying the ViewLocationFormats so Umbraco checks the standard folders and your new one/s.
Just checked with copilot which suggests:
Copy code
public class CustomViewLocation : ApplicationEventHandler
{
    protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
    {
        ViewEngines.Engines.OfType<RazorViewEngine>().First().ViewLocationFormats = new[]
        {
            "~/Views/CustomFolder/{0}.cshtml",
            "~/Views/Partials/BlockList/Components/{0}.cshtml",
            "~/Views/Shared/{0}.cshtml"
        };
    }
}
b
using view data is not best practise and can be easily avoided by usage of view models, view data has no validation on compile so it is easy to make errors with it ;p
s
this was a just a simple way of getting the desired result. You can always (over)engineer if you need πŸ™‚
b
@skttl following best practise being over engineering? πŸ€” It is not recommended in MVC using dataview so πŸ˜…
d
I'm here to learn as well as to help and welcome any tips @bielu . And to be fair I just asked co-pilot to give me a way of adding new paths. I'm sure there is an official Umbrao way to do it but in the meantime... So I understand you are suggesting using ViewModels instead of ViewData so : - its strongly typed - easier to maintain - can be validated - follows MVC best practice and of course has intelisense support
b
@Dean Leigh there is also that viewdata can be changed in async process so you might end up without data even when going through partials πŸ˜† so yeah my recommendation is to use view model to prevent it
d
I don't suppose you know the recommended way to consolidated your element types in a single directory for both Block List and Block Grid to answer the OP? I swear there was article at one point. Also I'm sure there were mentions of this for v14+.
b
I don't, I usually have different usage for both editors so views are not shared πŸ˜… but I would probably override the default render for both editors to look into same subdirectory for views for doctypes
m
Another approach might be to have a viewcomponent for rendering.. then you can pass your content and settings (view)models into the view component?
<vc:commonRender model="item.Content" settings="item.Settings" />
Ps not sure there is inherently anything wrong with using DataView.. as MVC rehydrates it's models from viewData, which is what you are doing any way when you
var settings = ViewData["settings"] as ContentModels.AccordionSetting;
? Also you do have to watch out for..
@await Html.PartialAsync(partialViewName, item.Content, new ViewDataDictionary(ViewData){ { "settings", item.Settings }});
As you are generating a
viewdDataDictionary
from the current
ViewData
you might already have
Settings
key added. And isn't
<partial name="{name}" model="item.Content" viewData="{vdd}" />
nicer? (though 6 of one half dozen the other....) I know my frontend devs seem more comfortable with tag helpers.. and in conjunction with the umbraco tag helpers..
<partial umb-if="...." .../>
seems to cause less issues than the
@{if (...){ @await Html.PartialAsync(...) }
b
@Mike Chambers ViewData can modified by any thread, there is nothing wrong with it, it just doesn't warranty you data in the end. so it is just not best practise as viewdata can be wiped before your view will be render πŸ˜„ "MVC rehydrates it's models from viewData" - as far I remember that is not fully true, but need dig sources, do you have any docs which pointing to that?
m
Will have a look to see if I can find what I thought I remembered.. also VDD passed to to a partial is scoped to the partial (not affecting viewData for others). Though understand your advice on ViewData.. if I didn't have to pass things around in ViewData I wouldn't (but also have to think economy of effort and time/cost viability πŸ™‚ )
I might be mis-remembering.. but where I had to do something it was around
ViewData.ModelState
where rehydration of the page model wasn't occurring, and I had to manually interogate the
ViewData.ModelState.TryGetValue("CallToDiscuss", out var callToDiscuss);
where the info was correct.. but that
Model.CallToDiscuss
was always posted back as
null
You can also pass a new viewDataDictionary...
Copy code
but you can also pass a new empty
var vdd = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary()) { { "noContainer", true } }; but then you might have model rehydration issues from from modelstate that you have wiped out
s
Thanks for input guys πŸ™‚ My solution looks something like this.
Copy code
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<IBlockReference>
@using Umbraco.Cms.Web.Common.PublishedModels
@{
  var gridModel = Model as BlockGridItem;
  var listModel = gridModel == null ? Model as BlockListItem : null;

  var block = gridModel?.Content as TextBlock ?? listModel?.Content as TextBlock;
  var settings = gridModel?.Settings as TextBlockSettings ?? listModel?.Settings as TextBlockSettings;

  if(block != null){
    <div class="text-block">
      <h1>@block.Title</h1>
      <div>@block.Text</div>
    </div>
  }
}
This way I can just call the partial like this.
<partial name="Blocks/TextBlock" model="block" />
p
@User May I suggest β€œfriends” instead of "guys"? We use gender inclusive language in this Discord. πŸ˜€
m
pattern matching and the new switch capabilities.. might make for better readability? ( cheating with copilot generated πŸ˜‰ )
Copy code
csharp
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<IBlockReference>
@using Umbraco.Cms.Web.Common.PublishedModels
@{
    var block = Model switch
    {
        BlockGridItem gridItem => gridItem.Content as TextBlock,
        BlockListItem listItem => listItem.Content as TextBlock,
        _ => null
    };

    var settings = Model switch
    {
        BlockGridItem gridItem => gridItem.Settings as TextBlockSettings,
        BlockListItem listItem => listItem.Settings as TextBlockSettings,
        _ => null
    };

    if (block is not null)
    {
        <div class="text-block">
            <h1>@block.Title</h1>
            <div>@block.Text</div>
        </div>
    }
}
s
Uhhh ... very nice πŸ™‚
3 Views