Add one document into multiple website nodes automatically
I am developing a Global - Enterprise level site with fairly normal requirements. However there seems to be some limitations out of the box.
One of MY requirements is that I stick to the native Umbraco functionality with limited or no package installs and minimal customization.
The reason - upgrades are a bitch!!! The closer I can keep the native functionality the better it will be when upgrading.
So the Requirement I'm trying to solve for:
This is one instance of Umbraco holding multiple sites. Each site is a country version (localized) for one company.
The writer/editor will be entering articles that are pertinent to the whole company. They should be able to enter the article ONCE, in ONE PLACE. On saving or publishing the article should be "duplicated" into the correct section for ALL the sites. This will give the local country editors the ability to change, or translate the content - delete- or whatever - specific to their needs without it affecting the other sites.
Given these requirement does anyone have a solution? Is there a "fairly" native way to get umbraco to do this? Through an event? Does the backoffice trigger the ContentServiceEvent?
You can use the ApplicationEventHandler class to create an event which fires when some content is published.
You can then detect what content is published by looking at its content type.
Then you can use the ContentService.Copy() method to copy it to other nodes within the site.
A very basic example below:
public class GenericContentEvents : ApplicationEventHandler
{
public GenericContentEvents()
{
ContentService.Published += ContentPublished;
}
public void ContentPublished(IPublishingStrategy sender, PublishEventArgs<IContent> e)
{
var article = e.PublishedEntities.FirstOrDefault(pe => pe.ContentType.Equals("YourArticleDocumentType"));
// Go get all the nodes which have the "parent article" type
// Foreach parent create child article e.g...
ApplicationContext.Current.Services.ContentService.Copy(article, newParentId, false);
}
}
This is a great answer. Fantastic help to start and will be marking this as an answer with a high five.
I am implementing it now and trying to test. It doesn't look as if my event is being fired. I tried wiring up the delegate in the constructor as well as ApplicationStarted(). Google search does not show any answers as most people simply did not setup their Class correctly.
Why can I not hit my debugging breakpoints anywhere in this class? Why are none of these events or methods firing? (Before I commented out the code to test if the constructor was firing.)
public class RegisterEvents : ApplicationEventHandler
{
public RegisterEvents()
{
ContentService.Published += ContentPublished;
}
protected override void ApplicationInitialized(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
{
base.ApplicationInitialized(umbracoApplication, applicationContext);
}
protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
{
//ContentService.Published += ContentPublished;
//WebApiConfig.Register(GlobalConfiguration.Configuration);
//RouteTable.Routes.MapPageRoute("Robots", "robots.txt", "~/robots.txt");
//BundleConfig.RegisterBundles(BundleTable.Bundles);
//base.ApplicationStarted(umbracoApplication, applicationContext);
base.ApplicationStarted(umbracoApplication, applicationContext);
//Umbraco.Core.Services.ContentService.Published += ContentService_Published;
}
protected override void ApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
{
base.ApplicationStarting(umbracoApplication, applicationContext);
}
private void ContentPublished(IPublishingStrategy sender, PublishEventArgs<IContent> e)
{
// Get the content that is being published
// The lambda expression gets only the type of content that you want to work with
var article = e.PublishedEntities.FirstOrDefault(content => content.ContentType.Equals("blogArticle"));
// Get this content Parent - We don't want to copy the same file in the same location.
var service = ApplicationContext.Current.Services.ContentService;
var parent = service.GetParent(article);
// Go get all the nodes which have the "parent article" type
var nodes = NodeNavigation.GetNodesByAlias("blogArticleFolder");
// Foreach parent create child article e.g...
foreach(var node in nodes)
{
if (node.Id != parent.Id)
{
ApplicationContext.Current.Services.ContentService.Copy(article, node.Id, false);
}
}
}
}
I have other sites using the same scenario with no problems.
Troubleshooting...
It seems to have been some strange behavior in VS2017. The project dropped its reference to my Core project that housed the Application Event Handler class.
Thank you for updating the query. I will look into it, but initially that doesn't look like it may work in my specific scenario - we'll see. The ContentService.Saved or Saving event seems like it would be more appropriate where it is a new item. I also see that I will have difficulty placing it in the correct location based on the following tree structure - its a user friendly structure for the editors but a nightmare for me especially because the editor can change the name of the month to their own language:
I'm going to keep documenting my experience here if everyone doesn't mind. I'm learning along the way... some things work as they are supposed to, some do not!
I came up with a solution to be able to update or map all of the same articles. This will allow the editor to change the article after saving or delete all related articles.
I added a Global ID to the DocType with data type of Label. (non-editable).
To add an id here, I did it in the Saving event - but only want to do it for this doc type and if it's new.
I used the queries above but they never returned the correct doctype, I kept getting a NULL return. So I added the Alias to the query:
var articles = e.SavedEntities.Where(content => content.ContentType.Alias.Equals("blogArticle"));
None of the documented solutions worked. So I simply tested on whether the Id existed for the new article:
foreach (var article in articles)
{
if (article.Id == 0)
{
// give the article a global ID
article.Properties["globalArticleID"].Value = System.Guid.NewGuid();
}
}
Glad you are making progress and thanks for updating your post so others can learn from it!
Unfortunately IContent and IPublishedContent are two different interfaces. So models generated by ModelBuilder are IPublishedContent but content returned by ContentService (which is what is used in the events you are tapping into) are IContent. Even though they appear similar, they aren't compatible so you can't convert from one to the other (for instance, IContent can represent unpublished content whereas IPublishedContent is only every for content that has been published and in the XML cache).
I am about to refactor my code so I figure I will share this before it ends up in a bunch of different spots...
Any input on better umbraco pattern is appreciated. Especially before I start refactoring.
So here is a visual representation of what I am doing - All global articles are entered in the Global Admin spot and then "copied" to the appropriate website depending on a "picker". If the directory tree does not exist, than that is also generated in the appropriate site.
I Initially had this code under the Publishing event, but if I made a change to the original Global Document, the change wasn't able to update to the websites. (Example: Adding another website in the picker).
private void ContentService_Published(IPublishingStrategy sender, PublishEventArgs<IContent> e)
{
/***** Publish from the Global Section *****/
var contentService = ApplicationContext.Current.Services.ContentService;
var blogHome = contentService.GetById(2160);
var articles = e.PublishedEntities.Where(content => content.ContentType.Alias.Equals("blogArticle")
&& content.Ancestors().Contains<IContent>(blogHome)); /* NodeId = 2113*/
foreach (var article in articles)
{
var articleGlobalId = article.Properties["globalArticleID"].Value;
var month = article.Parent();
var year = month.Parent();
var blogContainer = year.Parent();
var copytoSitesList = article.Properties["mapToWebsite"].Value;
var rootNodes = (article.Properties["mapToWebsite"].Value.ToString()).Split(',').ToList(); /* These are ids */
foreach (var root in rootNodes)
{
int rootId;
bool canUseRootId = int.TryParse(root, out rootId);
if (canUseRootId && contentService.HasChildren(rootId))/* has children but might not have blog */
{
//Check for blog folder
var rootBlogContainer = contentService.GetChildren(rootId).Where(child => child.ContentType.Alias.Equals("blogContainer"));/* NodeId = 2115*/
if (rootBlogContainer != null && rootBlogContainer.Count() == 1) /* has Blog */
{
var container = rootBlogContainer.FirstOrDefault();
//check to see if the article already exists
var existingArticles = contentService.GetDescendants(container.Id).Where(content => content.ContentType.Alias.Equals("blogArticle"));
var existingArticle = existingArticles.Where(a => a.Properties["globalArticleID"].Value.Equals(articleGlobalId)).FirstOrDefault();
if (existingArticle != null
&& existingArticle.Name.Equals(article.Name)
&& existingArticle.Properties["title"].Value.ToString().Trim()
.Equals(article.Properties["title"].Value.ToString().Trim()))
{
// article exists - update the article
var existingParent = contentService.GetParent(existingArticle);
contentService.Delete(existingArticle);
contentService.Copy(article, existingParent.Id, false);
}
else if (existingArticle == null)
{
// check for year
var currentYear = contentService.GetChildren(container.Id).Where(f => f.Name == year.Name);
if (currentYear != null && currentYear.Count() == 1)
{
var yearFolder = currentYear.FirstOrDefault();
// check month
var currentMonth = contentService.GetChildren(yearFolder.Id).Where(f => f.Name == month.Name);//TODO: Use Dictionary
if (currentMonth != null && currentMonth.Count() == 1)
{
var monthFolder = currentMonth.FirstOrDefault();
/***** Determine the Site to copy to *****/
contentService.Copy(article, monthFolder.Id, false);
}
else
{
var newMonth = contentService.GetById(contentService.Copy(month, yearFolder.Id, false).Id);
/***** Determine the Site to copy to *****/
contentService.Copy(article, newMonth.Id, false);
}
}
else
{
var newYear = contentService.GetById(contentService.Copy(year, container.Id, false).Id);
var newMonth = newYear.Children().FirstOrDefault();
//then article
contentService.Copy(article, newMonth.Id, false);
}
}
}
else
{
CopyFullBlogSection(contentService, article, blogContainer, contentService.GetById(rootId));
}
}
else /* No Blog Exists*/
{
CopyFullBlogSection(contentService, article, blogContainer, contentService.GetById(rootId));
}
}
}
}
I find it illogical that that PublishEvenArgs is returning a "list" of PublishedEntites. This would be much easier to use e as the object being publised, instead of wasting the resource to query the list. Mainly because I have noticed that this published event fires for every single "node" that was published.
Something like e.ContentType would be so much better.
PublishEventArgs returns an IEnumerable because you can either publish a single node using the typical "Save and Publish" button, or publish a node and its children by right clicking the node in the tree view and selecting Publish.
Instead of nesting into so many levels (e.g. Year > Month > Article Name) you could perhaps try making the content type of "Blog" display its children as a list view. Users can then search for articles with an in-built search box for the list view?
Aha! I see, I just stepped through publishing a year with all child nodes. You are correct, sir!
Thank you Alex for your support!!!
I initially started down the path of one Blog node with articles as children in a list. That is easier for me to manage.
But it's not easier for the editors. When an article is published in global it is pushed to the sites as Unpublished so that the local editors can translate or "localize" the content (context). In the list of hundreds of articles, this becomes unmanageable.
I am also going to need to create a notification event...
Trust me, the fact that each site is a different country with a different language and content has different context - plus that editor can only see his/her site is tough to solve for!!!
Here's another thing I found that ties in to my question about using the generated models.
Though I understand that IPublished Content and IContent are different, I can still use the "safer" ModelTypeAlias property from the generated model that will update with any database changes.
So instead of:
var articles = e.PublishedEntities.Where(content => content.ContentType.Alias.Equals("blogArticle")
&& content.Ancestors().Contains<IContent>(blogHome));
I can use:
var articles = e.PublishedEntities.Where(content => content.ContentType.Alias.Equals(BlogArticle.ModelTypeAlias)
&& content.Ancestors().Contains<IContent>(blogHome));
Add one document into multiple website nodes automatically
I am developing a Global - Enterprise level site with fairly normal requirements. However there seems to be some limitations out of the box.
One of MY requirements is that I stick to the native Umbraco functionality with limited or no package installs and minimal customization.
The reason - upgrades are a bitch!!! The closer I can keep the native functionality the better it will be when upgrading.
So the Requirement I'm trying to solve for:
This is one instance of Umbraco holding multiple sites. Each site is a country version (localized) for one company.
The writer/editor will be entering articles that are pertinent to the whole company. They should be able to enter the article ONCE, in ONE PLACE. On saving or publishing the article should be "duplicated" into the correct section for ALL the sites. This will give the local country editors the ability to change, or translate the content - delete- or whatever - specific to their needs without it affecting the other sites.
Given these requirement does anyone have a solution? Is there a "fairly" native way to get umbraco to do this? Through an event? Does the backoffice trigger the ContentServiceEvent?
You can use the ApplicationEventHandler class to create an event which fires when some content is published.
You can then detect what content is published by looking at its content type.
Then you can use the ContentService.Copy() method to copy it to other nodes within the site.
A very basic example below:
This is a great answer. Fantastic help to start and will be marking this as an answer with a high five.
I am implementing it now and trying to test. It doesn't look as if my event is being fired. I tried wiring up the delegate in the constructor as well as ApplicationStarted(). Google search does not show any answers as most people simply did not setup their Class correctly.
Why can I not hit my debugging breakpoints anywhere in this class? Why are none of these events or methods firing? (Before I commented out the code to test if the constructor was firing.)
Have you read the docs? They're quite helpful for this:
https://our.umbraco.org/Documentation/Reference/Events/Application-Startup
https://our.umbraco.org/Documentation/Reference/Events/ContentService-Events
The events definitely work as I use them all the time :)
NB. I wouldn't use FirstOrDefault to get your item, I'd use a loop as there may be multiple published items:
Yes, of course, thank you.
I have other sites using the same scenario with no problems.
Troubleshooting...
It seems to have been some strange behavior in VS2017. The project dropped its reference to my Core project that housed the Application Event Handler class.
Thank you for updating the query. I will look into it, but initially that doesn't look like it may work in my specific scenario - we'll see. The ContentService.Saved or Saving event seems like it would be more appropriate where it is a new item. I also see that I will have difficulty placing it in the correct location based on the following tree structure - its a user friendly structure for the editors but a nightmare for me especially because the editor can change the name of the month to their own language:
Copies to: ->
You guys are awesome!
I'm going to keep documenting my experience here if everyone doesn't mind. I'm learning along the way... some things work as they are supposed to, some do not!
I came up with a solution to be able to update or map all of the same articles. This will allow the editor to change the article after saving or delete all related articles.
I added a Global ID to the DocType with data type of Label. (non-editable).
To add an id here, I did it in the Saving event - but only want to do it for this doc type and if it's new.
I used the queries above but they never returned the correct doctype, I kept getting a NULL return. So I added the Alias to the query:
That worked.
Then I tried to determine if the article was new by following the documentation: https://our.umbraco.org/Documentation/Reference/Events/determining-new-entity
None of the documented solutions worked. So I simply tested on whether the Id existed for the new article:
That worked!
Question: Is there a way to use model generated from the UmbracoModelsBuilder.Api to get the blog Article?
example:
Glad you are making progress and thanks for updating your post so others can learn from it!
Unfortunately
IContent
andIPublishedContent
are two different interfaces. So models generated by ModelBuilder areIPublishedContent
but content returned by ContentService (which is what is used in the events you are tapping into) areIContent
. Even though they appear similar, they aren't compatible so you can't convert from one to the other (for instance, IContent can represent unpublished content whereasIPublishedContent
is only every for content that has been published and in the XML cache).I am about to refactor my code so I figure I will share this before it ends up in a bunch of different spots...
Any input on better umbraco pattern is appreciated. Especially before I start refactoring.
So here is a visual representation of what I am doing - All global articles are entered in the Global Admin spot and then "copied" to the appropriate website depending on a "picker". If the directory tree does not exist, than that is also generated in the appropriate site.
I Initially had this code under the Publishing event, but if I made a change to the original Global Document, the change wasn't able to update to the websites. (Example: Adding another website in the picker).
Something like
e.ContentType
would be so much better.PublishEventArgs returns an IEnumerable because you can either publish a single node using the typical "Save and Publish" button, or publish a node and its children by right clicking the node in the tree view and selecting Publish.
Instead of nesting into so many levels (e.g. Year > Month > Article Name) you could perhaps try making the content type of "Blog" display its children as a list view. Users can then search for articles with an in-built search box for the list view?
Aha! I see, I just stepped through publishing a year with all child nodes. You are correct, sir!
Thank you Alex for your support!!!
I initially started down the path of one Blog node with articles as children in a list. That is easier for me to manage.
But it's not easier for the editors. When an article is published in global it is pushed to the sites as Unpublished so that the local editors can translate or "localize" the content (context). In the list of hundreds of articles, this becomes unmanageable.
I am also going to need to create a notification event...
Trust me, the fact that each site is a different country with a different language and content has different context - plus that editor can only see his/her site is tough to solve for!!!
Here's another thing I found that ties in to my question about using the generated models.
Though I understand that IPublished Content and IContent are different, I can still use the "safer" ModelTypeAlias property from the generated model that will update with any database changes.
So instead of:
I can use:
It seems there is a problem with the Published event and future publishing.
I am trying to create a Bitly url on publish. It works fine in the event but when I set the article to future publish it doesn't fire.
EDIT:
I just set a breakpoint and it did fire and run fine.
The property value (link) is not showing up in the Content Section.!!??
Hmm not sure why it wouldn't show for nodes which are set to publish on a specific date.
Is the URL definitely getting set? I just don't see why it wouldn't show if it's being set.
is working on a reply...