Page tree and Asset tree are kept separate and that’s definitely a good thing. It’s totally up to the editor on how to structure both pages and folders.
However in big sites that may soon become problematic simply because pages are strictly dependant on the files that are displayed in its body. It’s very important to make the files easy to find and reuse later on.
Imagine that you’re builing a site with several sections and you want to upload specific image logos to each of the sections. The “For This Page” folder cannot be shared across several pages, like section and all of descendant pages. It could be used for one specific page instance, so it’s not a solution in that case.
You would probably create sections as child nodes to your Home Page and similarly create a folder for each section in the Assets Tree. Similar structure is used on Alloy demo site.
Later on you would probably figure out that you should let ContentEvents do that synchronization for you to always create a folder whenever you add a new section.
But what if your site contained thousands of sections?
In one of our projects that was actually the case and the customer found it extremely hard to find the files of their interest. He didn’t know the name of the file to look for, he simply knew that another division had probably put the file somewhere in one of the folders of their department section.
It was easy to find their department page but then, looking at the page tree to find the corresponding folder in the Asset pane.. was definitely not very user friendly…
We decided to help by introducing a permanent mapping between certain page types and their corresponding “Page Asset Libraries”. This is how it looks like for the editor:
When clicking the highlighted button the appropriate Section Folder is automatically selected in the Assets Pane (with all its ancestors). The customer liked it so much that they even gave this button a name – “the magic button” ; )
It required a few important steps to get it up and running.
1) Folder is created & holds corresponding Section Page reference.
Create a new folder in an appropriate tree node while creating a new section and store its ref in Section.ContentFolderReference property.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
private ContentReference AssignFolderReference(T content) { var targetFolderReference = this.GetTargetFolderReference(content.ContentLink); var folder = this.ContentRepository.GetDefault<TFolder>(targetFolderReference); folder.Name = content.Name; var routableFolder = (IRoutable)folder; routableFolder.RouteSegment = UrlSegment.GetUniqueURLSegment(folder); folder.StructureReference = content.ContentLink.ToReferenceWithoutVersion(); var reference = this.ContentRepository.Save(folder, SaveAction.Publish).ToReferenceWithoutVersion(); var writable = (IContentFolderReferenceContainer)content.CreateWritableClone(); writable.ContentFolderReference = reference; using (new UpdateDatePreservingScope()) { var changeTrackable = (IChangeTrackable)content; using (new ModifiedByUserScope(changeTrackable)) { this.ContentRepository.Save((IContent)writable, content.ResolveSaveAction(), AccessLevel.NoAccess); } } return reference; } |
2) Section page holds corresponding Folder reference.
In addition you have to track & maintain all possible changes (rename, delete etc…).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
protected void Published(T content) { if (ContentReference.IsNullOrEmpty(content.ContentFolderReference)) { return; } if (this.IsInCopyPasteOperation()) this.AssignFolderReference(content); var masterContent = this.ContentRepository.Get<T>( content.ContentLink.ToReferenceWithoutVersion(), LanguageSelector.Fallback("en", true)); var folder = this.ContentRepository.Get<ContentFolder>( content.ContentFolderReference.ToReferenceWithoutVersion(), LanguageSelector.MasterLanguage()).CreateWritableClone(); if (string.Equals(folder.Name, masterContent.Name)) { return; } folder.Name = masterContent.Name; this.ContentRepository.Save(folder, SaveAction.Publish, AccessLevel.NoAccess); } |
3) UI element to preselect the folder
Last but not least… you need a way to trigger this action through the UI.
First you have to declare a dojo command:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
define([ "dojo/_base/declare", "epi/shell/command/_Command" ], function (declare, _Command) { return declare([_Command], { order:null, name: "NavigateToCorrespondingFolder", label: "", tooltip: "Navigate To Corresponding Folder", iconClass: "epi-iconShare", canExecute: true, _execute: function () { dojo.publish("/assets/navigateToContent"); } }); }); |
Then you need to add a command provider:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
define([ "dojo/_base/declare", "dijit/form/Button", "epi-cms/component/command/_GlobalToolbarCommandProvider", "initialization/commands/NavigateToCorrespondingFolderCommand" ], function (declare, Button, _GlobalToolbarCommandProvider, NavigateToCorrespondingFolderCommand) { return declare([_GlobalToolbarCommandProvider], { constructor: function () { this.inherited(arguments); this.addToTrailing(new NavigateToCorrespondingFolderCommand({ label: "Navigate to corresponding folder" }), { showLabel: false, widget: Button, "class": "epi-mediumButton show-corresponding-folder-button" }); } }); }); |
And finally the trickiest part that actually does all the magic:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
define([ // dojo "dojo/_base/declare", "dojo/_base/lang", "dojo/when", "dojo/_base/array", "dojo/topic", "dojo/_base/connect", "dojo/Deferred", "dojo/aspect", "dijit/registry", // epi "epi/dependency", "epi-cms/core/ContentReference" ], function ( // dojo declare, lang, when, array, topic, connect, Deferred, aspect, registry, // epi dependency, ContentReference ) { return declare([], { currentContext: null, postscript: function () { this.inherited(arguments); this._navigateHandler = dojo.subscribe("/assets/navigateToContent", this, function() { this.tryNavigateToCorrespondingNode(this.currentContext); }); }, tryNavigateToCorrespondingNode: function (context) { if (this._isSupportedContent(context)) { //EPiServer will handle expand by himself return; } if (!this.expandedfolderStore) { this.expandedfolderStore = dependency.resolve("epi.storeregistry").get("app.expandedfolder"); } var self = this; self.set("editingListItem", null); when(this.expandedfolderStore.get(context.id), function (returnValue) { if (returnValue.folderId == null) { return; } self.set("currentTreeItem", null); var reference = ContentReference.toContentReference(returnValue.folderId).createVersionUnspecificReference(); when(self.store.get(reference), lang.hitch(self, function () { self.store.get(returnValue.parent).then(lang.hitch(this, function (r) { self.treeStoreModel.onItemChildrenReload(r); setTimeout(function () { self._tryExpandNode(reference); }, 500); })); })); }); }, contentContextChanged: function (context, callerData) { if (context.parentLink) { this.store.get(context.parentLink).then(function (r) { //hack we need to ask episerver store fo parent link using reference as string to ensure that string contentlink will be returned in result object }); } this.inherited(arguments); this.currentContext = context; }, _setupSelection: function () { when(this.getCurrentContext(), lang.hitch(this, function (ctx) { ////work around for content that will always be updated this.contentContextChanged(ctx, null); })); }, _tryExpandNode: function (reference) { var deferred = new Deferred(); var self = this; when(self.store.get(reference.createVersionUnspecificReference()), function (model) { self.treeStoreModel && when(self.treeStoreModel.canExpandTo(model), function (canExpand) { if (canExpand) { self._updateCommands(model, self.menuType.TREE); self._updateCommands(model, self.menuType.LIST); self.set("currentTreeItem", reference); self.set("currentListItem", null); self.treeStoreModel.onSelect(reference, true); deferred.resolve(model); } }); }); return deferred.promise; } }); }); |