Handle Episerver Projects Programmatically
Sometimes there is a need to modify a bunch of pages in the CMS programmatically but you do not want to publish those pages immediately nor leave them as drafts and at the same time you want to be able to track all the pages you changed easily. The option to this is to work with Episerver projects which will allow you to keep track of your modified pages and be able to publish them all at once when you are confident that the changes are ready to go. To do this and more you can use the projects repository Api which is explained here.
In this blog post, we are going to display the usage of this Api in order to replace existing values with other ones for a number of properties which belong to a set of pages of the same type. For this example, we are going to create an Episerver Job so we can execute it from the admin section several times if needed. So without further due lets begin.
First, we are going to create a class named ReplaceValuesInPages which will inherit from ScheduledJobBase
public class ReplaceValuesInPages : ScheduledJobBase
We will add the project repository, search helper (does not use find) , content repository, content version repository dependencies to the Job class and also three private variables to account for processed items, total items and error messages
private Injected<ProjectRepository> _projectRepository;
private Injected<ISearchHelper> _searchHelper;
private Injected<IContentRepository> _contentRepository;
private Injected<IContentVersionRepository> _contentVersionRepository;
private StringBuilder _strBuilder;
private int _totalCount;
private int _totalProcessed;
Then, we will create a constructor for this class which will setup this job as stoppable
public ReplaceValuesInPages()
{
IsStoppable = false;
}
The execute method of the job is described below. Pay special attention to the comments inside the method which explains in more detail what is happening, but in plain terms, it tries to find if a project was created before, if it finds one, it will redo the changes applied to all pages which were part of the project and then it will remove the project to create a new one in order to avoid conflict issues
public override string Execute()
{
// Initialize string builder to save issues
_strBuilder = new StringBuilder();
// Set a project name
const string projectName = "Replace Values in Pages";
// Find it using the name from the list of projects
var project = _projectRepository.Service.List().SingleOrDefault(x => x.Name == projectName);
// Check if project is not there and create a new one if is the case
var newProject = new Project { Name = projectName };
if (project == null)
{
_projectRepository.Service.Save(newProject);
}
else
{
// If a project exists redo changes in the modified pages of this projects to avoid conflict issues
RedoPreviousItemsInProjects(project);
// Delete the project found and then save the new project so we will always start from scratch
_projectRepository.Service.Delete(project.ID);
_projectRepository.Service.Save(newProject);
}
// Try again to return the latest project which should be empty
project = _projectRepository.Service.List().SingleOrDefault(x => x.Name == projectName);
if (project == null)
{
return "Could not create project";
}
// Create as many asynchronous tasks as you want to process pages and replaces values based on a page type
var tasks = new List<Task>
{
Task.Factory.StartNew(() => ProcessPagesReplacingValues<BlogDetailPage>(project,
new[] {"More About Us", "About Us"},
new[] {"Contact Us Here", "Contact Us"}))
};
// Wait for all threads to finish
while (!Task.WhenAll(tasks).IsCompleted)
{
// Report status to Job screen
OnStatusChanged($"Processing pages: {_totalProcessed}/{_totalCount}");
}
// If there are errors, show it at the end of the process, if not print the success message
return _strBuilder.Length >= 1 ? _strBuilder.ToString() : "Finished processing pages";
}
The redo previous items in project method will search for all pages that belong to the project and get the latest modified version of the page and if that version is in checked in status, it will remove it using the content repository class
private void RedoPreviousItemsInProjects(Project project)
{
var pages = _projectRepository.Service.ListItems(project.ID);
foreach (var page in pages)
{
var latestVersion = _contentVersionRepository.Service.List(page.ContentLink)
.OrderByDescending(x => x.Saved)
.FirstOrDefault(version => version.IsMasterLanguageBranch);
if (latestVersion != null && latestVersion.Status == VersionStatus.CheckedIn)
{
_contentVersionRepository.Service.Delete(latestVersion.ContentLink);
}
}
}
The Process Pages Replacing Values method will search all not deleted pages which have the navigation title or name with the values we want to search. Then we process each one of the pages found and we editing them if and only if we can find the corresponding values for the navigation title and name properties using the new values provided. If the pages were processed/modified, we will add them to a list of project items and then at the end we will save all project items to the project. Pay special attention to the comments inside the method which can explain a little bit more about what the method does
private void ProcessPagesReplacingValues<T>(Project project, IReadOnlyList<string> toCheck, IReadOnlyList<string> toReplace) where T : SitePage
{
// Find all not deleted pages which belong to an specific type and whose values match the ones we are trying to check
// We are going to check for two properties Navigation Title and Name of the page
var pages = _searchHelper.Service.SearchAllPagesByType<T>().
Where(x => x.NavigationTitle == toCheck[0] || x.Name == toCheck[1])
.Where(x => !x.IsDeleted).ToList();
// Initialize counter
_totalCount += pages.Count;
// Maintain a list of project items
var projectItems = new List<ProjectItem>();
// Iterate over the list of found pages
foreach (var oldPage in pages)
{
// Increment counter
_totalProcessed++;
try
{
// Create a clon of the page to process so we can modify it
var page = oldPage.CreateWritableClone() as T;
var process = false;
// If page somehow is null continue to another page
if (page == null)
{
continue;
}
// Get latest version of the page before any modification
var latestVersion = _contentVersionRepository.Service.List(page.ContentLink)
.OrderByDescending(x => x.Saved)
.FirstOrDefault(version => version.IsMasterLanguageBranch);
// Check if the Navigation title has a value and if is the value that we need to replace
// If is the case, we will replace it with the value we provided
if (!string.IsNullOrEmpty(toCheck[0]) && page.NavigationTitle == toCheck[0])
{
page.NavigationTitle = toReplace[0];
process = true;
}
// Same scenario as above but for the Name of the page
if (!string.IsNullOrEmpty(toCheck[1]) && page.Name == toCheck[1])
{
page.Name = toReplace[1];
process = true;
}
// If page was not modified continue
if (!process)
{
continue;
}
// If the page was modified, check in which status it is and save it accordingly
var result = latestVersion.ContentLink;
if (latestVersion.Status == VersionStatus.Published)
{
_contentRepository.Service.Save(page, SaveAction.ForceNewVersion, AccessLevel.NoAccess);
}
if (latestVersion.Status == VersionStatus.AwaitingApproval)
{
_contentRepository.Service.Save(page, SaveAction.Reject | SaveAction.SkipValidation, AccessLevel.NoAccess);
}
if (latestVersion.Status != VersionStatus.CheckedIn)
{
result = _contentRepository.Service.Save(page, SaveAction.CheckIn, AccessLevel.NoAccess);
}
// Get updated page reference
var pageUpdated = _contentRepository.Service.Get<T>(result);
// Add the updated page as a project item and add it to the project
var newProjectItem = new ProjectItem(project.ID, pageUpdated);
projectItems.Add(newProjectItem);
}
catch (Exception e)
{
// If there is an error add it to the string builder variable
_strBuilder.Append($"Page: {oldPage.Name} of type: {typeof(T).Name} with error: {e.Message},");
}
}
// Save all items added to the project list using the project repository class
_projectRepository.Service.SaveItems(projectItems.ToArray());
}
Finally, we will show the code for the method Search All Pages by Type which is part of the Search Helper that allows us to search all pages for a specific type but without using Episerver Find
public List<T> SearchAllPagesByType<T>() where T : PageData
{
//Create an empty list to response
var response = new List<T>();
//Define a criteria collection to do the search
var criterias = new PropertyCriteriaCollection();
//create criteria for searching page types
var criteria = new PropertyCriteria
{
Condition = CompareCondition.Equal,
Type = PropertyDataType.PageType,
Name = EnumSearchName.PageTypeID.ToString(),
Value = ServiceLocator.Current.GetInstance<IContentTypeRepository>().Load<T>().ID.ToString()
};
// Add criteria to collection
criterias.Add(criteria);
// Searching from start page
var repository = ServiceLocator.Current.GetInstance<IPageCriteriaQueryService>();
var pages = repository.FindPagesWithCriteria(ContentReference.StartPage.ID == 0 ?
ContentReference.RootPage : ContentReference.StartPage, criterias);
// Adding result to the list
foreach (var page in pages) response.Add((T)page);
response = response.OrderByDescending(x => (int)x["PagePeerOrder"]).ToList();
return response;
}
And that is it. Now, if you execute the job, it will modify all the pages of an specific type that have the values we define in the navigation title and name properties and replace those values with new ones, but instead of publishing the pages, it will add all modified pages to a project so that an editor can review them before publishing. If you have any question let me know. I hope it will help someone and as always keep learning !!!
Leave a Reply