Handle Episerver Projects Programmatically

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 !!!

Written by:

Jorge Cardenas

Developer with several years of experience who is passionate about technology and how to solve problems through it.

View All Posts

Leave a Reply