November Happy Hour will be moved to Thursday December 5th.

Programmatically remove expired block content from edit mode

Vote:
 

I am trying to make a scheduled job that will remove expired content from content area or xhtml string to clean up the edit mode of unused blocks, but not move it to the recycle bin because maybe someday it will be used again. 

I found many posts here on World and on various blogs about getting the references to specified content but nothing that would show me the way to actually remove them. I am close but something is still missing. I also think there is more cleaner way of sloving this but couldn't find any Repository or Service that would clean up list of ReferenceInformation returned by GetReferencesToContent method.

Here is the code:

var contentLoader = ServiceLocator.Current.GetInstance<IContentLoader>();
var contentTypeRepository = ServiceLocator.Current.GetInstance<IContentTypeRepository>();
var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();
var contentModelUsage = ServiceLocator.Current.GetInstance<IContentModelUsage>();

var contentType = contentTypeRepository.Load<MyBlockType>();
var contentUsages = contentModelUsage.ListContentOfContentType(contentType);

// get distinct content references without version
var contentReferences = contentUsages
	.Select(x => x.ContentLink.ToReferenceWithoutVersion())
	.Distinct()
	.ToList();

// fetch data from DB
var instances = contentReferences
	.Select(contentReference => contentLoader.Get<IContent>(contentReference)).Where(x => (x as IVersionable)?.StopPublish < DateTime.Now)
	.Select(x => x.ContentLink);

foreach (var contentReference in instances)
{
	//I assume that if the next collection of ReferenceInformation could be cleared and saved as such, it would solve the problem without looping through all page properties
	var usages = contentRepository.GetReferencesToContent(contentReference, true);
	foreach (var usage in usages)
	{
		var original = contentRepository.Get<ContentData>(usage.OwnerID, usage.OwnerLanguage);
		var clone = (ContentData)original.CreateWritableClone();
		var oldLink = contentRepository.Get<BlockData>(usage.ReferencedID).ToString(); // to be replaced with string.Empty if found in XhtmlString
		foreach (var property in content.Property.Where(x => x.PropertyValueType == typeof(ContentArea) || x.PropertyValueType == typeof(XhtmlString)))
		{
			if (property.PropertyValueType == typeof(ContentArea))
			{
				if (((ContentArea)original[prop])?.Items?.Any(x => x.ContentLink.CompareToIgnoreWorkID(usage.ReferencedID)) ?? false)
				{
					//remove content area item with the contentLink as same as expired content.
				}
			}
			if (prop.PropertyValueType == typeof(XHtmlString))
			{
				//parse string and replace with string.Empty if content reference to expired content was found.
			}
            
			//Here I've tried to save, check out, publish, force new version, bitwise action on enum (but this enum was not marked with [Flags] attribute so in reality not sure if it even should work.
			contentRepository.Save((IContent)clone, SaveAction.Publish, AccessLevel.NoAccess);
		
			//without next piece of code GetReferencesToContent in second for each loop returns usage as it was never tampered with.
			//on top of all, ContentSoftLinkIndexer is marked as Internal API and could be changed without notice.
			var contentsoftlinkindexer = ServiceLocator.Current.GetInstance<ContentSoftLinkIndexer>();
            var contentSoftLinkRepo = ServiceLocator.Current.GetInstance<IContentSoftLinkStatusService>();

            var links = contentsoftlinkindexer.GetLinks(content).Where(x => !x.ReferencedContentLink.CompareToIgnoreWorkID(contentreference)).ToList();
            contentSoftLinkRepo.SaveLinkStatus(links);
			//This resets usage of the expired content but doesn't remove it from content Area in edit mode
		}
	}
}
#202383
Mar 25, 2019 12:13
Vote:
 

Hi Goran,

Honestly, I think this is the cleanest way of solving this problem, I'm fairly sure there is no nice, one-stop RemoveReferencesToContent() method! 😄

Couple of comments, based on your code comments:

Although the SaveAction enum is not decorated with the flags attribute it is a collection of "primary commands (CheckIn, Publish, Schedule) that defines the main action and option flags (ForceNewVersion, ForceCurrentVersion) that provide additional information about the save operation" (directly from the documentation). So you can do, for example:

contentRepository.Save(content, SaveAction.Publish | SaveAction.ForceNewVersion | SaveAction.SkipValidation, AccessLevel.NoAccess);

Also, how are you removing the content from the content area? It should be automatically updating the SoftLinks when you publish which happens in the DefaultContentProviderDatabase (although it first checks if any PropertyData have IsModified set to true).

I made some quick modifications to your code and the following seems to work:

var contentType = contentTypeRepository.Load<MyBlockType>();
var contentUsages = contentModelUsage.ListContentOfContentType(contentType);

var contentReferences = contentUsages.Select(x => x.ContentLink.ToReferenceWithoutVersion())
    .Distinct().ToList();

foreach (var contentLink in contentReferences)
{
    if (!contentRepository.TryGet(contentLink, out IContent content))
    {
        continue;
    }

    var versionable = content as IVersionable;

    if (versionable?.StopPublish == null || versionable.StopPublish.Value > DateTime.Now)
    {
        continue;
    }

    var refInfos = contentRepository.GetReferencesToContent(contentLink, true);

    foreach (var refInfo in refInfos)
    {
        if (!contentRepository.TryGet(refInfo.OwnerID, refInfo.OwnerLanguage, out ContentData owner))
        {
            continue;
        }

        owner = (ContentData)owner.CreateWritableClone();

        if (owner == null)
        {
            continue;
        }

        bool updated = false;

        foreach (var property in owner.Property.Where(x => x.PropertyValueType == typeof(ContentArea)))
        {
            var contentArea = owner[property.Name] as ContentArea;

            var item = contentArea?.Items.FirstOrDefault(x =>
                x.ContentLink.CompareToIgnoreWorkID(contentLink));

            if (item == null)
            {
                continue;
            }

            contentArea.Items.Remove(item);
            updated = true;
        }

        if (updated)
        {
            contentRepository.Save((IContent)owner, SaveAction.Publish | SaveAction.ForceNewVersion, AccessLevel.NoAccess);
        }
    }
}

I only checked with content areas but XHtmlStrings should work very similarly.

/Jake

#202686
Edited, Mar 29, 2019 1:09
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.