I'm using Orchard 1.9.3 and have set up a couple of custom ContentTypes that mimic the standard Page type with an Autoroute and Layout Part etc.
These types of pages should never be set as the homepage so I want to hide just the Set as home page field of the Autoroute part but only for my custom types. I'm not sure what the most efficient way is to go about this. Can I target this field specifically in a placement file?
You can override the Parts.Autoroute.Edit.cshtml and include some custom logic:
#{
var canSetAsHomePage = true;
var myTypesToDisableHomePageFor = ["MyCustomContentType", "AnotherCustomContentType"];
if (myTypesToDisableHomePageFor.Contains(Model.ContentType)) {
canSetAsHomePage = false;
}
}
// ..
#if (!Model.IsHomePage && canSetAsHomePage) {
if (AuthorizedFor(Permissions.SetHomePage)) {
// ..
For this to work you also have to add an extra property to Orchard.Autoroute.ViewModels.AutoroutePartEditViewModel:
public class AutoroutePartEditViewModel {
...
public string ContentType { get; set; }
}
and make sure to set it in the Editor method of Orchard.Autoroute.Drivers.AutoroutePartDriver:
var viewModel = new AutoroutePartEditViewModel {
CurrentUrl = part.DisplayAlias,
Settings = settings,
ContentType = part.ContentItem.ContentType
};
I'm using Orchard 1.9.3 and followed some tutorials on how to create a custom normal Element in Orchard. I couldn't find any specifically on creating container elements so I dug around a bit in the source and this is what I have so far:
Elements/Procedure.cs
public class Procedure : Container
{
public override string Category
{
get { return "Content"; }
}
public override string ToolboxIcon
{
get { return "\uf0cb"; }
}
public override LocalizedString Description
{
get { return T("A collection of steps."); }
}
public override bool HasEditor
{
get { return false; }
}
}
Drivers/ProcedureElementDriver.cs
public class ProcedureElementDriver : ElementDriver<Procedure> {}
Services/ProcedureModelMap
public class ProcedureModelMap : LayoutModelMapBase<Procedure> {}
Views/LayoutEditor.Template.Procedure
#using Orchard.Layouts.ViewModels;
<div class="layout-element-wrapper" ng-class="{'layout-container-empty': getShowChildrenPlaceholder()}">
<ul class="layout-panel layout-panel-main">
<li class="layout-panel-item layout-panel-label">Procedure</li>
#Display()
#Display(New.LayoutEditor_Template_Properties(ElementTypeName: "procedure"))
<li class="layout-panel-item layout-panel-action" title="#T("Delete {{element.contentTypeLabel.toLowerCase()}} (Del)")" ng-click="delete(element)"><i class="fa fa-remove"></i></li>
<li class="layout-panel-item layout-panel-action" title="#T("Move {{element.contentTypeLabel.toLowerCase()}} up (Ctrl+Up)")" ng-click="element.moveUp()" ng-class="{disabled: !element.canMoveUp()}"><i class="fa fa-chevron-up"></i></li>
<li class="layout-panel-item layout-panel-action" title="#T("Move {{element.contentTypeLabel.toLowerCase()}} down (Ctrl+Down)")" ng-click="element.moveDown()" ng-class="{disabled: !element.canMoveDown()}"><i class="fa fa-chevron-down"></i></li>
</ul>
<div class="layout-container-children-placeholder">
#T("Drag a steps here.")
</div>
#Display(New.LayoutEditor_Template_Children())
All of this is more or less copied from the Row element. I now have a Procedure element that I can drag from the Toolbox onto my Layout but it is not being rendered with my template, even though I can override the templates for the other layout elements this way, and I still can't drag any children into it. I had hoped that simply inheriting from Container would have made that possible.
I essentially just want to make a more restrictive Row and Column pair to apply some custom styling to a list of arbitrary content. How can I tell Orchard that a Procedure can only be contained in a Column and that it should accept Steps (or some other element) as children?
I figured out how to make container and containable elements from looking at Mainbit's layout module.
The container elements require some additional Angular code to make them work. I still need help figuring out how to limit which elements can be contained!
Scripts/LayoutEditor.js
I had to extend the LayoutEditor module with a directive to hold all of the Angular stuff pertaining to my element:
angular
.module("LayoutEditor")
.directive("orcLayoutProcedure", ["$compile", "scopeConfigurator", "environment",
function ($compile, scopeConfigurator, environment) {
return {
restrict: "E",
scope: { element: "=" },
controller: ["$scope", "$element",
function ($scope, $element) {
scopeConfigurator.configureForElement($scope, $element);
scopeConfigurator.configureForContainer($scope, $element);
$scope.sortableOptions["axis"] = "y";
}
],
templateUrl: environment.templateUrl("Procedure"),
replace: true
};
}
]);
Scripts/Models.js
And a Provider for Orchard's LayoutEditor to use:
var LayoutEditor;
(function (LayoutEditor) {
LayoutEditor.Procedure = function (data, htmlId, htmlClass, htmlStyle, isTemplated, children) {
LayoutEditor.Element.call(this, "Procedure", data, htmlId, htmlClass, htmlStyle, isTemplated);
LayoutEditor.Container.call(this, ["Grid", "Content"], children);
//this.isContainable = true;
this.dropTargetClass = "layout-common-holder";
this.toObject = function () {
var result = this.elementToObject();
result.children = this.childrenToObject();
return result;
};
};
LayoutEditor.Procedure.from = function (value) {
var result = new LayoutEditor.Procedure(
value.data,
value.htmlId,
value.htmlClass,
value.htmlStyle,
value.isTemplated,
LayoutEditor.childrenFrom(value.children));
result.toolboxIcon = value.toolboxIcon;
result.toolboxLabel = value.toolboxLabel;
result.toolboxDescription = value.toolboxDescription;
return result;
};
LayoutEditor.registerFactory("Procedure", function (value) {
return LayoutEditor.Procedure.from(value);
});
})(LayoutEditor || (LayoutEditor = {}));
This specifically is the line that tells the element what it can contain:
LayoutEditor.Container.call(this, ["Grid", "Content"], children);
ResourceManifest.cs
Then I made a resource manifest to easily make these available in Orchard's module.
public class ResourceManifest : IResourceManifestProvider
{
public void BuildManifests(ResourceManifestBuilder builder)
{
var manifest = builder.Add();
manifest.DefineScript("MyModule.Models").SetUrl("Models.js").SetDependencies("Layouts.LayoutEditor");
manifest.DefineScript("MyModule.LayoutEditors").SetUrl("LayoutEditor.js").SetDependencies("Layouts.LayoutEditor", "MyModule.Models");
}
}
By default, .SetUrl() points to the /Scripts folder in your module/theme.
Handlers/LayoutEditorShapeEventHandler.cs
Finally, I added this handler to load my scripts on the admin pages that use the Layout Editor.
public class LayoutEditorShapeEventHandler : IShapeTableProvider
{
private readonly Work<IResourceManager> _resourceManager;
public LayoutEditorShapeEventHandler(Work<IResourceManager> resourceManager)
{
_resourceManager = resourceManager;
}
public void Discover(ShapeTableBuilder builder)
{
builder.Describe("EditorTemplate").OnDisplaying(context =>
{
if (context.Shape.TemplateName != "Parts.Layout")
return;
_resourceManager.Value.Require("script", "MyModule.LayoutEditors");
});
}
}
Hopefully, this will help someone out in the future. However, I still don't know how to make it so that my Container will only contain my Containable or that my Containable will only allow itself to be contained by my Container. It seems like adjusting LayoutEditor.Container.call(this, ["Grid", "Content"], children); would have been enough to achieve this, but it's not. More help is still welcome.
First of all, thank you for this answer. I found it really helpful. Still, I ended up having problems to restrict where my container element could be placed and what could be placed inside of it.
I've noticed that those restrictions are made based on the category of the element. Canvas, Grid, Row, Column or Content.
Orchard goes through all categories and runs some code to understand where the items inside that category can be placed.
Anything outside Orchard's Layout Category is a Content. If you have a lot of different custom categories for a variety of custom elements, they are still Contents in Orchard's eyes. So... For every category you have, Orchard's gonna run some code and say that every item inside that category is actually a Content and they all end up having the same placement rules.
I didn't want any of my custom container to be placeable inside another custom container, and I didn't want anything other then content being placed inside my custom containers, so I ended up doing the following steps:
Go to your Procedure.cs file and change your class' category.
public override string Category => "Container";
Go to your Models.js file and change the value in the "dropTargetClass" property.
this.dropTargetClass = 'layout-common-holder layout-customcontainer';
Go to the LayoutEditor.Template.ToolboxGroup.cshtml file (you could create your own in your theme) and change the value in the "ui-sortable" attribute in the ul element.
ui-sortable="category.name == 'Container' ?
$parent.getSortableOptions(category.name) : $parent.getSortableOptions('Content')"
Go to the Toolbox.js file and edit the "getSortableOptions" function, so it contains a case for your newly created "Container" category. Pay attention to where the "layout-customcontainer" class appears bellow. I wanted to remove the ability of placing grids, and other layout elements inside my container, so I had to change their cases too.
switch (type) {
case "Container":
parentClasses = [".layout-column", ".layout-common-holder:not(.layout-customcontainer)"];
placeholderClasses = "layout-element layout-container ui-sortable-placeholder";
break;
case "Grid":
parentClasses = [".layout-canvas", ".layout-column", ".layout-common-holder:not(.layout-customcontainer)"];
placeholderClasses = "layout-element layout-container layout-grid ui-sortable-placeholder";
break;
case "Row":
parentClasses = [".layout-grid"];
placeholderClasses = "layout-element layout-container layout-row row ui-sortable-placeholder";
break;
case "Column":
parentClasses = [".layout-row:not(.layout-row-full)"];
placeholderClasses = "layout-element layout-container layout-column ui-sortable-placeholder";
floating = true; // To ensure a smooth horizontal-list reordering. https://github.com/angular-ui/ui-sortable#floating
break;
case "Content":
parentClasses = [".layout-canvas", ".layout-column", ".layout-common-holder"];
placeholderClasses = "layout-element layout-content ui-sortable-placeholder";
break;
case "Canvas":
parentClasses = [".layout-canvas", ".layout-column", ".layout-common-holder:not(.layout-container)"];
placeholderClasses = "layout-element layout-container layout-grid ui-sortable-placeholder";
break;}
Run the Gulpfile.js task, so your changes are placed inside Orchard's LayoutEditor.js file.
Now, you have a container element with some custom restrictions.
I hope it's not to late to be useful to you.
I'm adding a custom wrapper to widgets in placement.info like this:
<Match ContentType="Widget">
<Place Parts_Common_Body="Content:5;Wrapper=Wrapper_AsideWidget" />
</Match>
This works just fine, but I need to to limit the application of the custom wrapper to only widgets in a few specific zones. Right now they're being applied to widgets in all zones. What's the best way to achieve this? It would be perfect if the Match element could be scoped to a zone but I don't think that's possible.
Any advice or suggestions?
UPDATE
Here's the final solution I came up with. It applies the custom wrapper to any widgets in the aside zones. Just dropped the class into the theme.
public class AsideWidgetShapeProvider : IShapeTableProvider
{
public void Discover(ShapeTableBuilder builder)
{
builder.Describe("Widget")
.OnDisplaying(displaying =>
{
var shape = displaying.Shape;
ContentItem contentItem = shape.ContentItem;
if (contentItem != null)
{
var zoneName = contentItem.As<WidgetPart>().Zone;
if (zoneName == "AsideFirst" || zoneName == "AsideSecond")
{
shape.Metadata.Wrappers.Add("Wrapper_AsideWidget");
}
}
});
}
}
You can create a Shape Table Provider that describes the behavior of the Parts_Common_Body shape and applies your wrapper conditionally. Just add a class such as the following to your module, and Orchard will process it when it builds the shape table.
Example:
using Orchard.ContentManagement;
using Orchard.DisplayManagement.Descriptors;
using Orchard.Widgets.Models;
namespace MyModule {
public class ShapeTable : IShapeTableProvider {
public void Discover(ShapeTableBuilder builder) {
builder.Describe("Parts_Common_Body")
.OnDisplaying(ctx => {
var shape = ctx.Shape;
// Parts_Common_Body has a ContentPart property, so you can
// do this to get at the content item.
var contentItem = ((IContent)shape.ContentPart).ContentItem;
if (contentItem.ContentType == "Widget") { // content type to check for
var widgetPart = contentItem.As<WidgetPart>();
if (widgetPart.Zone == "AsideFirst") { // zone to check for
// Condition is met, let's add the wrapper.
ctx.ShapeMetadata.Wrappers.Add("Wrapper_AsideWidget");
}
}
});
}
}
}
I want to create alternates for content item based on its tag value.
For example, I want to create an alternate called List-ProjectionPage-tags-special
Searching the nets directs me to implement a new ShapeDisplayEvents
Thus, I have
public class TagAlternatesFactory : ShapeDisplayEvents
{
public TagAlternatesFactory()
{
}
public override void Displaying(ShapeDisplayingContext context)
{
}
}
In the Displaying method, I believe I need to check the contentItem off the context.Shape and create an alternate name based off of that (assuming it has the TagsPart added to the content item).
However, what do I do with it then? How do I add the name of the alternate? And is that all that's needed to create a new alternate type? Will orchard know to look for List-ProjectionPage-tags-special?
I took a cue from Bertrand's comment and looked at some Orchard source for direction.
Here's my implementation:
public class TagAlternatesFactory : ShapeDisplayEvents
{
public override void Displaying(ShapeDisplayingContext context)
{
context.ShapeMetadata.OnDisplaying(displayedContext =>
{
var contentItem = displayedContext.Shape.ContentItem;
var contentType = contentItem.ContentType;
var parts = contentItem.Parts as IEnumerable<ContentPart>;
if (parts == null) return;
var tagsPart = parts.FirstOrDefault(part => part is TagsPart) as TagsPart;
if (tagsPart == null) return;
foreach (var tag in tagsPart.CurrentTags)
{
displayedContext.ShapeMetadata.Alternates.Add(
String.Format("{0}__{1}__{2}__{3}",
displayedContext.ShapeMetadata.Type, (string)contentType, "tag", tag.TagName)); //See update
}
});
}
}
This allows an alternate view based on a tag value. So, if you have a project page that you want to apply a specific style to, you can simply create your alternate view with the name ProjectionPage_tag_special and anytime you want a projection page to use it, just add the special tag to it.
Update
I added the displayedContext.ShapeMetadata.Type to the alternate name so specific shapes could be overridden (like the List-ProjectionPage)
I saw 2 different way to create web parts for sharepoint. Which one is preferred by most?
http://msdn.microsoft.com/en-us/library/aa973249%28office.12%29.aspx
Anything involving VSeWSS is just going to end in pain, so method 1 is definitely out. Method 2 isn't ideal either, as setting up html elements as controls becomes unmanageable at a level just beyond what you see in that demo. I use a fairly simple generic base class that takes a user control as a type parameter and lets me keep all the layout nicely seperated from the sharepoint infrastructure. If you are creating pages/web parts programatically most of the web part xml turns out to be optional also.
public abstract class UserControlWebPart<T> : Microsoft.SharePoint.WebPartPages.WebPart where T:UserControl
{
protected UserControlWebPart()
{
this.ExportMode = WebPartExportMode.All;
}
protected virtual void TransferProperties(T ctrl)
{
var tc = typeof(T);
var tt = this.GetType();
foreach (var p in tt.GetProperties()) {
if (p.IsDefined(typeof(ControlPropertyAttribute), true)) {
foreach (var p2 in tc.GetProperties()) {
if (p2.Name == p.Name) {
p2.SetValue(ctrl, p.GetValue(this, null), null);
}
}
}
}
}
protected override void CreateChildControls()
{
string controlURL = ControlFolder+typeof(T).Name+".ascx";
var ctrl = Page.LoadControl(controlURL) as T;
TransferProperties(ctrl);
this.Controls.Add(ctrl);
}
protected virtual string ControlFolder
{
get {
return "~/_layouts/UserControlWebParts/";
}
}
}
For the few web parts I've written, I guess I've gone more with method #2 than method #1. Seems more straightforward and has the potential to be reused outside of the SharePoint environment (depending on the depth of your business logic).