Pages

Friday 16 July 2021

Umbraco - Copy Grid content

In Umbraco 7, we had Vorto to handle multi-lingual fields.

In Umbraco 8, Variants were introduced, as well as the split screen editor where you can have 2 languages side by side, for easier translation.

My problem?

Normal text fields can be easily copied left to right by selecting them and copy/pasting the content.
What's been bugging me since the very beginning, is that it's a real hassle to copy an entire grid to a new language.

As a Content Editor in charge of translating this content, I am required to rebuild the entire grid-structure. To translate the grid in the screenshot above, I would have to :

  • add an XL row
  • add a headline component
    • copy/paste the text (or translate it on the fly)
  • add a richtext component
    • copy/paste the text (or translate it on the fly)
  • add an M-M row
  • add an image component
    • figure out which image they used, if I want to use the same one
  • add a richtext component
    • copy/paste the text (or translate it on the fly)
  • ... and so on
This gets tedious very fast. What I really want to be able to do is copy the entire grid structure from left to right and then just translate the portions of text that I need to change.

This feature was requested in October 2019. It became up-for-grabs February 2020 (meaning, anyone who felt like it, could do an attempt at implementing it and making a pull request).
April 2021 someone picked it up and started working on it. The pull request is still pending further improvements.

Disclaimer(s) about my "solution"

  • It was tested on Umbraco 8.13. It should work on older versions and will probably remain working on future versions (minor tweaks could be needed, but are unlikely since it's so isolated).
  • When upgrading to a newer version, you'll probably need to redo the modifications.
  • It is a bit hacky (modifying files in Umbraco), but works fine for all the projects I'm working on.
  • It does not take into account that custom propertyeditors might subscribe to certain events emitted by the grid when e.g. adding a new row or control.

My solution

My quick and dirty solution to have the option of copy/pasting the entire structure of the grid from left to right (or even from item A to item B), consists of adding custom code to 2 files in the Umbraco codebase.

What will happen is:
  • A button "Copy Grid" is displayed next to the "Reorder" button. When pressed, the JSON describing the entire grid is copied to localStorage.
  • A button "Paste Grid" is displayed when no rows have yet been added to the grid and will simply grab the JSON from localStorage again. 

The good news is that these files are .js and .html, so there's no compilation step necessary, making this change something a 5 year old (with advanced computer skills) could do.

\Umbraco\js\umbraco.controllers.js

We have to place are code somewhere, so find the definition of the method called "toggleSortMode" (around line 19.000) and insert the following 2 function-definitions above it.
// Start Grid Copy/Paste
var gridCopyAlias = "grid-value-copy";

$scope.copyGridModel = function copyGridModel() {
    localStorageService.set(gridCopyAlias, $scope.model.value);

    var msg = 'Grid successfully copied to clipboard.';
    notificationsService.success(msg);		
}

$scope.pasteGridModel = function pasteGridModel() {
    try {
        var modelValue = localStorageService.get(gridCopyAlias);
        if (modelValue) {
            $scope.model.value = modelValue;							
        } else {
            var msg = 'Couldn\'t find a copied Umbraco Grid. Make sure to copy an existing Grid first.';
            notificationsService.error(msg);    
        }
    } catch (err) {
        var msg = 'Something went wrong pasting the Grid.';
        notificationsService.error(msg, err);
    }
}
// End Grid Copy/Paste

Then, scroll up a bit (~250 lines) to find the definition of Angular controller "Umbraco.PropertyEditors.GridController".
'use strict';
angular.module('umbraco').controller('Umbraco.PropertyEditors.GridController', function ($scope, localizationService, gridService, umbRequestHelper, angularHelper, $element, eventsService, editorService, overlayService, $interpolate) {

and inject 2 additional services at the end:
  • notificationsService : to be able to add warnings, errors in the backoffice
  • localStorageService : something to serve as a clipboard while copy/pasting
It should now look like 
'use strict';
angular.module('umbraco').controller('Umbraco.PropertyEditors.GridController', function ($scope, localizationService, gridService, umbRequestHelper, angularHelper, $element, eventsService, editorService, overlayService, $interpolate, notificationsService, localStorageService) {

\Umbraco\Views\propertyeditors\grid\grid.html

All the way at the top, you will find the definition for the ReOrder button.
    <umb-editor-sub-header appearance="white" ng-if="showReorderButton()">
        <umb-editor-sub-header-content-right>
            <umb-button action="toggleSortMode()" button-style="link" icon="icon-navigation" label-key="{{reorderKey}}" type="button">
            </umb-button>
        </umb-editor-sub-header-content-right>
    </umb-editor-sub-header>

We will add 2 of our own buttons here, to call the methods we just added to the controller.
Just replace the entire section with the code below
    <umb-editor-sub-header appearance="white" ng-if="!showReorderButton()">
        <umb-editor-sub-header-content-right>
            <umb-button action="pasteGridModel()" button-style="link" icon="icon-arrow-down" label="Paste Grid" type="button">
            </umb-button>
        </umb-editor-sub-header-content-right>
    </umb-editor-sub-header>

    <umb-editor-sub-header appearance="white" ng-if="showReorderButton()">
        <umb-editor-sub-header-content-right>
            <umb-button action="copyGridModel()" button-style="link" icon="icon-arrow-up" label="Copy Grid" type="button">
            </umb-button>

            <umb-button action="toggleSortMode()" button-style="link" icon="icon-navigation" label-key="{{reorderKey}}" type="button">
            </umb-button>
        </umb-editor-sub-header-content-right>
    </umb-editor-sub-header>

Make sure to put these modified files in your solution, or they will be overwritten the next time you do a deploy.

Wednesday 1 July 2020

Umbraco - Enabling Language Fallback by default

Sometimes you need a fresh pair of eyes to see an obvious solution when you're stuck looking in the wrong direction.

(Skip the backstory to the technical implementation if you prefer technical stuff over the story)

Backstory

Earlier, I talked about using Modelsbuilder partial classes to enable Language Fallback on all variant properties, so your View Designers would have an easier time of using fallback without even realizing it.

The reason I want this is:

I want (Razor) View Designers to be able to use

Model.Title

and not

Model.Value<string>("title", fallback: Fallback.ToLanguage)

I want them to
  • not need to know HOW to get fallback on a poperty
  • not need to know WHEN to use it (when a property is variant)
  • not have an excuse to FORGET to use it
I ALSO don't want to use my previously discussed approach where I need to implemented each Variant property in a partial class.

I wanted to find a better solution. I wanted Modelsbuilder to generate the .Value() method using Language Fallback by default.
I went as far as implementing it and making a Pull Request for ModelsBuilder, based on an AppSetting to enable this behaviour.

The PR was basically refused and a long discussion ensued wether or not what I wanted had merit and my solution was the correct approach.

Until one day, Ronald Barendse commented (paraphrased) "Why don't you just override the default Umbraco behaviour?"

My eyes opened and I went to work (and completed it unexpectedly fast).

Implementation

The default behaviour resides in the PublishedValueFallback class. To enable a solution just for this situation, they made the TryGetValue method virtual, so you can override it with whatever you need.

using System.Linq;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Services;
using Umbraco.Web.Models.PublishedContent;

namespace Dpw.Eworld2.Foundation.Umbraco.PublishedContent

{
    public class CustomPublishedValueFallback : PublishedValueFallback
    {
        public CustomPublishedValueFallback(ServiceContext serviceContext, IVariationContextAccessor variationContextAccessor)
            : base(serviceContext, variationContextAccessor)
        {
            
        }

        public override bool TryGetValue<T>(IPublishedContent content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value, out IPublishedProperty noValueProperty)
        {
            //When no fallback, use ToLanguage by default
            if (!fallback.Any(f => f == Fallback.DefaultValue || f == Fallback.Language || f == Fallback.Ancestors))
            {
                fallback = Fallback.ToLanguage;
            }

            return base.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out value, out noValueProperty);
        }
    }
}

Bascially, what I do is : when no fallback of any sort is requested, I put fallback to Language Fallback. Nothing else needs to change.

To enable this custom class instead of the default one, you register it in your Startup class as follows:

composition.RegisterUnique<IPublishedValueFallback, CustomPublishedValueFallback>();

I was then able to toss my PR, my custom built ModelsBuilder-version, all remaining partial classes and never need to even think about which property will be Variant.

Thursday 11 June 2020

Remote debug Umbraco on Azure Web App

The title is a bit misleading, because you can debug ANY Azure Web App, not just when running Umbraco, but I had to keep it a bit Umbraco related.

When everything works

If everything was always working, I wouldn't be writing about it today. There's plenty of guides to show you how to do remote debugging, when everything is fine.

In my experience - more often than not - there's always some crucial little step, preventing you from quickly attaching a debugger and finding the exact cause of that elusive error that you just can't reproduce on your local environment.
Remote debugging has saved me HOURS of trying to get to that one error that only one of your users is experiencing in production.

Under normal circumstances, you would
  • publish your Web App from Visual Studio in debug mode. 
  • go to your Cloud Explorer, scroll down to your web app
  • click Attach debugger (this will activate remote debugging if not already active)
 

What can possible go wrong?

Well, you could:
  • not be able to publish for some reason
  • have forgotten to put it in Debug mode
  • not see your Web App in Cloud Explorer, even though your seeing everything else and you DO have access in the Azure Portal and IT already verified that everything is working
    (clearly, I'm speaking hypothetically, because this never happens)

Do it all yourself

Enable remote debugger

First, you'll need to enable remote debugging on your Web App.
  • Go to your Web App in the Azure Portal.
  • Go to Configuration > General Settings
  • Enable Remote Debugging and choose your version of Visual Studio

Download the PublishProfile

The PublishProfile contains all endpoints and credentials you'll need for accessing this Web App.
You can download it from the Overview blade.


The file looks like this.
Obviously, I've fudged it a bit, so you wouldn't get any ideas of trying to mess with my app.
<publishData>
	<publishProfile profileName="[yourApp] - Web Deploy" publishMethod="MSDeploy" publishUrl="[yourApp].scm.azurewebsites.net:443" msdeploySite="[yourApp]" userName="$[yourApp]" userPWD="[yourAppPassword]" destinationAppUrl="http://[yourApp].azurewebsites.net" SQLServerDBConnectionString="" mySQLDBConnectionString="" hostingProviderForumLink="" controlPanelLink="http://windows.azure.com" webSystem="WebSites">
		<databases />
	</publishProfile>
	<publishProfile profileName="[yourApp] - FTP" publishMethod="FTP" publishUrl="ftp://[yourAppFTP].ftp.azurewebsites.windows.net/site/wwwroot" ftpPassiveMode="True" userName="[yourApp]\$[yourApp]" userPWD="[yourAppPassword]" destinationAppUrl="http://[yourApp].azurewebsites.net" SQLServerDBConnectionString="" mySQLDBConnectionString="" hostingProviderForumLink="" controlPanelLink="http://windows.azure.com" webSystem="WebSites">
		<databases />
	</publishProfile>
	<publishProfile profileName="[yourApp] - ReadOnly - FTP" publishMethod="FTP" publishUrl="ftp://[yourAppFTP]dr.ftp.azurewebsites.windows.net/site/wwwroot" ftpPassiveMode="True" userName="[yourApp]\$[yourApp]" userPWD="[yourAppPassword]" destinationAppUrl="http://[yourApp].azurewebsites.net" SQLServerDBConnectionString="" mySQLDBConnectionString="" hostingProviderForumLink="" controlPanelLink="http://windows.azure.com" webSystem="WebSites">
		<databases />
	</publishProfile>
</publishData>
The interesting bits are 
  • url - this should be "[yourApp].azurewebsites.net" (make sure there's no ".scm." part)
  • userName - make sure to take the fully qualified one : "[yourApp]\$[yourApp]"
  • userPWD

Attach your debugger

  • In Visual Studio, go to Debug > Attach to process (CTRL-ALT-P)
  • In the Connection target, paste your Url plus port 4022 (for VS2017, port 4024 for VS2019)
  • In the popup-window, paste username and password 
If your credentials are correct and there is no company firewall blocking your outgoing request, you should be able to see all running processes on the remote machine.
We want to attach to the w3wp.exe. If running a .Net Core application, this will be dotnet.exe.


And that's it, you are now remote debugging Umbraco (or whatever you're working on) and can put breakpoints where you think the problems occur.

Troubleshooting

Now, there are numerous things in the preceding steps that can go wrong and possibly many more that I haven't even encountered yet myself.

Cannot attach debugger

You get an error after entering the url of the Web App, or the popup asking for credentials does not appear.

It's most likely a connectivity issue.
  • Check your corporate firewall.
  • Check if it works from home (while not on any VPN), or tether via your mobile.
  • Check your Web App for IP filtering.

Cannot activate breakpoints

The version of your code doesn't match the version running on your Web App.
  • Try republishing the latest version.
  • Try finding the source branch that matches this release.
If the problem you're investigating is in a specific class library, try building a new version of that project only and push that .dll and it's accompanying .pdb via Kudu or FTP (you have the ftp-credentials in the PublishProfile you downloaded earlier).

Make a backup of the existing .dll, so you can quickly revert.

Thursday 4 June 2020

DocTypeGridEditor - Remove default DocType option

As I mentioned in my previous post, I'd be checking to get rid of the default "Doc Type" option in the popup below.
It allows your editors to choose any DocType (marked as "Element") and we can think of reasons why you wouldn't want this option to be available to them.


Even if you didn't mention it in your grid.editors.config.js, the "Doc Type" option is always there.

Turns out it's defined in the manifest-file (App_plugins\DocTypeGridEditor\package.manifest). If you make that gridEditors-property into an empty array, the option will no longer be there.

{
  "gridEditors": [],
  "javascript": [
    "~/App_Plugins/DocTypeGridEditor/Js/doctypegrideditor.resources.js",
    "~/App_Plugins/DocTypeGridEditor/Js/doctypegrideditor.services.js",
    "~/App_Plugins/DocTypeGridEditor/Js/doctypegrideditor.controllers.js",
    "~/App_Plugins/DocTypeGridEditor/Js/doctypegrideditor.directives.js"
  ],
  "css": [
    "~/App_Plugins/DocTypeGridEditor/Css/doctypegrideditor.css"
  ]
}

I hope anyone can benefit from this. See you around!

DocTypeGridEditor - partial view was not found

We've been working with DocTypeGridEditor for years, but just once in a while, there's an issue that takes more than 5 minutes to isolate.

I'm going to assume you know what I'm talking about, for an intro to DTGE will take too much time and can be found elsewhere.

Below is the popup that the users get when adding a control to the grid.


Embed, Image, Macro and Rich text are there by default. Headline and Quote is something we replaced in our base project. Widget is something specific for the project I'm working on.

The problem is the "Doc Type" option. It's not configured in the grid.editors.config.js where you control which controls the editors can pick. It must be added behind the scenes by the DTGE implementation somehow.

For some reason, however, if one of our editors picked "Doc Type" instead of one of the others, it yields the following error:
System.InvalidOperationException: The partial view 'richTextBlock' was not found or no view engine supports the searched locations. The following locations were searched:
~/Views/ContentPage/richTextBlock.aspx
~/Views/ContentPage/richTextBlock.ascx
~/Views/Shared/richTextBlock.aspx
~/Views/Shared/richTextBlock.ascx
~/Views/ContentPage/richTextBlock.cshtml
~/Views/ContentPage/richTextBlock.vbhtml
~/Views/Shared/richTextBlock.cshtml
~/Views/Shared/richTextBlock.vbhtml
~/Views/Partials/richTextBlock.cshtml
~/Views/MacroPartials/richTextBlock.cshtml
~/Views/richTextBlock.cshtml
   at System.Web.Mvc.HtmlHelper.FindPartialView(ViewContext viewContext, String partialViewName, ViewEngineCollection viewEngineCollection)
   at System.Web.Mvc.HtmlHelper.RenderPartialInternal(String partialViewName, ViewDataDictionary viewData, Object model, TextWriter writer, ViewEngineCollection viewEngineCollection)
   at System.Web.Mvc.Html.PartialExtensions.Partial(HtmlHelper htmlHelper, String partialViewName, Object model, ViewDataDictionary viewData)
   at Our.Umbraco.DocTypeGridEditor.Web.Extensions.HtmlHelperExtensions.RenderDocTypeGridEditorItem(HtmlHelper helper, IPublishedElement content, String editorAlias, String viewPath, String previewViewPath, Boolean isPreview)
   at ASP._Page_app_plugins_doctypegrideditor_render_doctypegrideditor_cshtml.Execute() in C:\Projects\DPW\Website\ProjectV8.Site\app_plugins\doctypegrideditor\render\doctypegrideditor.cshtml:line 28
   at System.Web.WebPages.WebPageBase.ExecutePageHierarchy()
   at System.Web.Mvc.WebViewPage.ExecutePageHierarchy()
   at System.Web.WebPages.WebPageBase.ExecutePageHierarchy(WebPageContext pageContext, TextWriter writer, WebPageRenderingBase startPage)
   at Umbraco.Web.Mvc.ProfilingView.Render(ViewContext viewContext, TextWriter writer) in d:\a\1\s\src\Umbraco.Web\Mvc\ProfilingView.cs:line 25
   at System.Web.Mvc.Html.PartialExtensions.Partial(HtmlHelper htmlHelper, String partialViewName, Object model, ViewDataDictionary viewData)
   at ASP._Page_Views_Partials_grid_editors_base_cshtml.Execute() in C:\Projects\DPW\Website\ProjectV8.Site\Views\Partials\grid\editors\base.cshtml:line 20

Using the richTextBlock directly works fine, so after some head-scratching I discovered that we usually declare our components as follows:
  {
    "name": "Headline",
    "alias": "headline",
    "view": "/App_Plugins/DocTypeGridEditor/Views/doctypegrideditor.html",
    "icon": "icon-coin",
    "render": "/App_Plugins/DocTypeGridEditor/Render/DocTypeGridEditor.cshtml",
    "config": {
      "allowedDocTypes": [
        "headlineBlock"
      ],
      "nameTemplate": "",
      "enablePreview": true,
      "viewPath": "/Views/Partials/Grid/Editors/",
      "previewViewPath": "/Views/Partials/Grid/Editors/",
      "previewCssFilePath": "",
      "previewJsFilePath": ""
    }
  }
Some debugging showed me that the "Doc Type" definition, which is added automatically, even if you want it or not, is this:
  {
    "name": "Doc Type",
    "alias": "docType",
    "view": "/App_Plugins/DocTypeGridEditor/Views/doctypegrideditor.html",
    "render": "/App_Plugins/DocTypeGridEditor/Render/DocTypeGridEditor.cshtml",
    "icon": "icon-item-arrangement",
    "config": {
      "allowedDocTypes": [],
      "nameTemplate": "",
      "enablePreview": false,
      "viewPath": "/Views/Partials/Grid/Editors/DocTypeGridEditor/",
      "previewViewPath": "/Views/Partials/Grid/Editors/DocTypeGridEditor/Previews/",
      "previewCssFilePath": "",
      "previewJsFilePath": ""
    }
  }
The difference is the viewPath-property, ending in /DocTypeGridEditor/, which we do not have. Historical reasons, I guess.

The only way to override this default behavious is by manually adding the DocType configuration to the grid.editors.config.js, with the correct path of course.

Now, if I find a way to eliminate the Doc Type option altogether, I'll let you know.

Tuesday 21 April 2020

Easy tool for Pagespeed optimization

Last october, I talked at Duugfest about Google Pagespeed and how to optimize your page for scoring high ...  having a fast, mobile-friendly website.

I even blogged about it in februari, after winning the Google Hackathon and in more technical detail  here.

One of the things I mentioned at Duugfest was that I had made a "tool" (an MVC ActionFilterAttribute) that would post-process all rendered HTML and "do something" with all the images.
Hang tight, I'll get less vague in a minute.

More than one of you asked if I could share the code for the filter, and it took me until now to get around to that. So, I created a nuget package with my ActionFilter and called it Our.Umbraco.PageSpeed. The intention is to add more tricks as I devise them along the way.

The reason for this one-filter-catches-all approach is 2-fold.

1. Lazy-loading

Google Pagespeed Insights recommends lazy loading your images. It's very easy to use LaySizes for it. So easy, in fact, that I won't repeat anything you can easily read in their README.

Now, what you'll have to do, is go through all your code (views, partial views, ...) and tweak your images a bit.
Basically, your images look like this:

<img data-src="image.jpg" class="lazyload" />

There are reasons NOT to do this however:

  • it's boring, repetitive work, prone to errors.
  • you may miss an image here and there. Nobody will notice, until you run Pagespeed insights.
  • you don't control ALL places where images are rendered. E.g. a content editor can put an image inside a richtext-field.

A filter will just parse the HTML and replace all images. None are missed, no typo's are made.

2. Serve images in better formats

Google suggest serving images in WEBP, as it is a better format than jpg, png, ...
Not all browsers support it, however, so you may want to resort to using <picture> instead of <img> and provide multiple images, out of which your browser can choose, depending on it's capabilities.

This would mean something along the lines of:

<picture>
  <source type="image/webp" src="webp-version of image.jpg" />
  <source data-src="image.jpg" class="lazyload" />
</picture>
If only there was a way of converting images to webp without requiring our content editors to manage multiple versions of each image.

But wait, there IS : ImageProcessor is baked in in Umbraco and has a plugin for Webp.


Combining it all 

Now, if we combine the possibility of

  • replacing all our img-tags on-the-fly by source-tags,
  • providing a way to convert to Webp,
  • lazy loading the result.
we get the following:

<picture>
  <source type="image/webp" data-src="image.jpg?format=webp&quality=70" class="lazyload" />
  <source data-src="image.jpg" class="lazyload" />
</picture>

How to achieve this?

  1. Install LazySizes in your project as per their instructions.
  2. Install nuget ImageProcessor.Plugins.WebP
  3. Install nuget Our.Umbraco.PageSpeed
  4. Decorate your controller with the LazyLoadFilter-attribute
[LazyLoadFilter]
public class MyRenderMvcController : RenderMvcController
{
    public override ActionResult Index(RenderModel model)
    {
        //Do stuff here. Or not, see if I care.
        return base.Index(model);
    }
}

And for all those DocTypes that you didn't write a Controller for. How do you make sure they get their fair share of the fun?

For these, you can set the default Controller to your MyRenderMvcController, and it will also benefit from the LazyLoadFilter-attribute.

DefaultRenderMvcControllerResolver.Current.SetDefaultControllerType(typeof(MyRenderMvcController));

If you want to give this a go and it doesn't work for you for any reason, give me a shout and we'll figure something out.

Wednesday 15 April 2020

Umbraco 8 - Language fallback for Grid properties

In my previous post (which was hanging around in draft for months until I finally got around to finishing it) I mentioned that my way-of-working with Language Fallback does not work with Grid properties.

Grids are never really empty

The reason for this is that when you use property.Value("propertyAlias", fallback: Fallback.ToLanguage), the only scenario where fallback occurs, is when the value of that property is empty.
When you make a new language version of an item and you do not put anything in the grid, it still is not empty, because what constitutes as an "empty" grid, actually looks like this.

{
  "name": "1 column layout",
  "sections": [
    {
      "grid": "12",
      "rows": []
    }
  ]
}

A second problem I encountered, is that the HtmlHelper-extension that is used to render your grid in a view only accepts a propertyAlias and under the hood it goes straight for the property in question, not through .Value(), preventing you from adding a similar workaround as in my previous article.

Fortunately Umbraco is Open Source, and the code for GetGridHtml is easily located.

public static MvcHtmlString GetGridHtml(this HtmlHelper html, IPublishedContent contentItem, string propertyAlias, string framework)
{
    if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentNullOrEmptyException(nameof(propertyAlias));

    var view = "Grid/" + framework;
    var prop = contentItem.GetProperty(propertyAlias);
    if (prop == null) throw new NullReferenceException("No property type found with alias " + propertyAlias);
    var model = prop.GetValue();

    var asString = model as string;
    if (asString != null && string.IsNullOrEmpty(asString)) return new MvcHtmlString(string.Empty);

    return html.Partial(view, model);
}

A new way of rendering

So, all I had to do was create my own class with my own extension method.

Now what happens is:
  • I get the value of the grid-property. With language fallback indicated, but that never happens, because a grid is never empty.
  • If the grid is functionally "empty", I get the English value of the same grid-property.
  • Normal processing resumes.

In my views, where - until now - I used the standard way of rendering grids, I had to add a using statement and change the name to my own extension method.

@using MyCode.Umbraco.Web.Extensions;
@inherits UmbracoViewPage<ContentPage>
@{
    Layout = "BasePage.cshtml";
}
<div>
    @Html.GetFallbackGridHtml(Model, ContentPage.GetModelPropertyType(c => c.Body).Alias, "site")
</div>

For those interested, here's the entire class. No rocket science, but it made my job just a little easier again.

using System.Linq;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using Newtonsoft.Json.Linq;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Web;
using Umbraco.Web.Composing;

namespace MyCode.Umbraco.Web.Extensions
{
    public static class GridExtensions
    {
        public static bool IsGridEmpty(object gridContent)
        {
            var asJToken = gridContent as JToken;
            if (asJToken != null)
            {
                //Check all sections
                var sections = asJToken["sections"] as JArray;
                if (sections != null && sections.Any())
                {
                    //If any section has any row --> not empty
                    foreach (var section in sections)
                    {
                        var rows = section["rows"] as JArray;
                        if (rows != null && rows.Any())
                        {
                            return false;
                        }
                    }
                }
            }

            return true;
        }

        public static MvcHtmlString GetFallbackGridHtml(this HtmlHelper html, IPublishedContent content, string propertyAlias, string framework = "bootstrap3")
        {
            var gridContent = content.Value(propertyAlias, fallback: Fallback.ToLanguage);

            //If gridcontent is "empty", do a fallback to defaultLanguage
            if (IsGridEmpty(gridContent))
            {
                var defaultLanguage = Current.Services.LocalizationService.GetDefaultLanguageIsoCode();
                gridContent = content.Value(propertyAlias, defaultLanguage);
            }

            return html.GetFallbackGridHtml(gridContent, framework);
        }

        public static MvcHtmlString GetFallbackGridHtml(this HtmlHelper html, object gridContent, string framework = "bootstrap3")
        {
            if (gridContent == null)
            {
                return new MvcHtmlString(string.Empty);
            }

            var view = "Grid/" + framework;
            return html.Partial(view, gridContent);
        }
    }
}