To convert an existing grid value to a new block list value with corresponding blocks, you can use the MigrationsService
, which has a Convert(GridDataModel,object?)
method.
The first parameter is an instance of GridDataModel
from my Skybrud.Umbraco.GridData package. If you don't already have parsed the grid value to a GridDataModel
, you can use the package's IGridFactory
to create the model via _gridFactory.CreateGridModel(null, null, gridData, false)
.
The second parameter is an optional owner value (eg. a reference to the page that holds the original grid value). I'm not using this parameter in my examples, but it might be relevant to have if grid controls are also based on data from the page holding the grid value.
The Convert
method iterates through the grid, and then checks the type of each grid control through a switch case statement, which will delegate the conversion of each grid editor to a separate methods (helps keeping the code clean 😄). If the method encounters a grid editor it hasn't yet been implemented to recognize, it will throw an exception. I've used a try and error approach, so if throws an exception, I know there is a new scenario that I need to handle.
For this Gist, I'm converting three different types of grid editors - a rich text grid editor, a video and then a quote.
The ConvertQuote
is used to convert a quote based grid control to a corresponding BlockQuote
model. Starting out, the method will read out the raw values from the original JSON object. Even if you had strongly typed grid models for the old site, you it might not make sense to copy them over to the new site, which is why I've gone for the raw approach.
After validating that the grid control actually had a value for the quote, I'm creating a new object for the conntet data via _blockListFactory.CreateContentData<BlockQuote>(control)
. This ensures that the content data is created with the correct content type. This method returns an instance of BlockListContentData
, which we can then use to add new properties to. Eg:
var content = _blockListFactory
.CreateContentData<BlockQuote>(control)
.Add("quote", quote)
.Add("origin", byline);
Most if not all our blocks also has a default settings type - eg. where users can hide the block from the frontend (#togglegate). So we can create a new settings data instance like:
var settings = _blockListFactory
.CreateDefaultSettings(control);
If you have a specific settings type you wish to use, there is also a CreateSettingsData<MySettings>(control)
method in the block list factory.
Finally we need to add the content and settings data as a new item to the block list:
blockList.AddItem(content, settings);
which is shorthand for writing:
blockList.Items.Add(new BlockListItem(content, settings));
The RTE based grid editor is a bit more complex, as it may contain reference to both content and media. For my case, I didn't really need to handle content references, but I did need to handle media references.
In Umbraco 7 and 8, media URLs would have a format like /media/{id}/file.ext
, but from Umbraco 9 and up, the format is instead /media/{hash}/file.ext
. If the migration doesn't convert these references, they will no longer work.
Notice that I've had a seperate migration of media not covered in Gist, so all the media files are in their new, proper locatons (hash based folder structure).
Media references can be both links (<a>
) and images (<img>
), so my ConvertRte
method handles both.
In my case, the old site had a video based grid editor for inserting YouTube videos. The editor supported adding multiple videos, but was configured to only allow one video per grid control. The ConvertVideo
method will therefore throw an exception if it encounters multiple videos in the same control.
On the new site, we're using my Limbo.Umbraco.YouTube package, so we've already added the necessary credentials to appsettings.json
.
Then via GoogleHttpService
(from Skybrud.Social.Google) and .YouTube()
(from Skybrud.Social.Google.YouTube), I can fetch information about the video from the YouTube video and save it in the same format as if using the Limbo.Umbraco.YouTube package directly.
In our case, users can also add a bit of extra information with the video, so we a block list in a block list, where the outer block has all the additonal information as well as a property with a new block list for the video.
A part of what makes this work, is also the BlockListFactory
class and related models. For instance, the factory contains the CreateContentData<T>(Guid key)
and CreateContentData<T>(GridControl control)
methods, which will create a new BlockListContentData
instance representing the content data.
The method is generic, meaning that it will use <T>
for finding the correct content type. To make this work, T
should be a model extending PublishedElementModel
, and ideally be generated by either my Limbo.Umbraco.ModelsBuilder, or the Models builder that ships with Umbraco.
In a similar way, the factory also has the CreateSettingsData<T>(Guid key)
and CreateSettingsData<T>(GridControl control)
methods for creating new instances of BlockListSettingsData
representing the settings data.
Each content data and settings data must have unique UDIs. If you have duplicate UDIs across different pages, the content may start to leak from one page to another, which isn't ideal.
So to ensure unique UDIs, you can use the Guid.NewGuid()
method for the basis for the UDIs. In my case, I neeed to handle the conversion in a reproduceable way. Eg. if the input hasn't changed, the output should change either. Using Guid.NewGuid()
would prevent that from working.
In the old grid model, the individual grid controls don't have any information that uniquely identifies them, but each row does have a unique GUID key, which we can use as a basis for creating unique GUIDs. But as a grid row may contain multiple grid areas, and each area may contain multiple grid controls, we also need to take into account.
For the site I was migrating, the grid was configured in a way where it wasn't possible to add multiple areas to a row, I could skip checking for this. But is was still possibel to add multiple controls to an area. So the BlockListContentData(GridControl,IPublishedContentType)
constructor will use the control's index in the parent's list of controls to generate unique GUID keys.
int index = control.Area.Controls.IndexOf(control);
Guid key = index == 0 ? Guid.Parse(control.Row.Id) : SecurityUtils.GetMd5Guid(control.Row.Id + "#" + index);
The first control re-uses the GUID key of the grid row, while the key for any additional control is based on the MD5 hash of a combination of the GUID key of the grid row and the control's index in the area's list of controls. And luckily MD5 hashes can be represented as a GUID.