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");
}
}
});
}
}
}
Related
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.
In Orchard CMS I have a service that pulls data from an external data source, and loads the data into an Orchard Content Part. The Part has a migration that welds it with a title part, and I have a route so that my controller is being hit via a URL:
I am using a controller to access the item via a URL, much like the Blog Part controller. However I can't render my part...
The Blog Controller does similar to the following:
var asset = _assetService.Get(1234);
if (asset == null) return HttpNotFound();
var model = _services.ContentManager.BuildDisplay(asset);
return new ShapeResult(this, model);
But if I do this, the 'BuildDisplay' method looks for asset.ContentItem but this is null, despite deriving my part from 'ContentPart'.
What do I need to do to get my data to display?
If I understand correctly, you are trying to display only one part, and not a whole content item.
To display a single shape, you can do the following:
private readonly IAssetService _assetService;
public MyController(IShapeFactory shapeFactory, IAssetService assetService) {
_assetService = assetService;
Shape = shapeFactory;
}
public dynamic Shape { get; set; }
public ActionResult MyAction(int assetId) {
var asset = _assetService.Get(1234);
if (asset == null) return HttpNotFound();
// the shape factory can call any shape (.cshtml) that is defined
// this method assumes you have a view called SomeShapeName.cshtml
var model = Shape.SomeShapeName(asset);
return new ShapeResult(this, model);
}
!!Note:
This does not kick of the (display)driver of the part, it only returns the .cshtml with the given model
By having my part deriving from ContentPart, I can use the following Controller method:
private readonly IAssetService _assetService;
private readonly IOrchardServices _services;
public MyController(IShapeFactory shapeFactory, IAssetService assetService, IOrchardServices services) {
_assetService = assetService;
_services = services;
Shape = shapeFactory;
}
public dynamic Shape { get; set; }
public ActionResult MyAction(int assetId) {
var asset = _assetService.Get(1234);
if (asset == null) return HttpNotFound();
// this method assumes you have a view called Parts.Asset.cshtml (see the AssetPartDriver)
var model = _services.ContentManager.New("Asset");
var item = contentItem.As<AssetPart>();
item.Populate(asset) // Method that just populates the service loaded object into the ContentPart
return new ShapeResult(this, _services.ContentManager.BuildDisplay(item));
}
This will use the 'AssetPartDriver':
public class AssetPartDriver : ContentPartDriver<AssetPart>
{
protected override DriverResult Display(AssetPart part, string displayType, dynamic shapeHelper)
{
return ContentShape("Parts_Asset", () => shapeHelper.Parts_Asset()); // Uses Parts.Asset.cshtml
}
}
And in conjunction with the 'Placement.info' file renders on the screen:
<Placement>
<Match ContentType="Asset">
<Match DisplayType="Detail">
<Place Parts_Asset="Content"/>
</Match>
</Match>
</Placement>
The migration file combines my web service part with other Orchard parts:
public class Migrations : DataMigrationImpl
{
public int Create()
{
ContentDefinitionManager.AlterTypeDefinition("Asset", cfg => cfg
.WithPart("AssetPart")
.WithPart("AutoroutePart", builder => builder
.WithSetting("AutorouteSettings.AllowCustomPattern", "True"))
.Listable()
.Securable()
.Creatable(false));
ContentDefinitionManager.AlterPartDefinition("AssetPart", part => part
.WithDescription("A part that contains details of an individual Web Service loaded asset."));
return 1;
}
}
These additional parts are not yet used, but can be populated during creation and displayed individually using the placement file.
This is the first step of what I was trying to achieve!!
I have an Orchard content handler that calls
Filters.Add(new Orchard.ContentManagement.Handlers.ActivatingFilter<MyPart>("User"));
in its constructor to weld MyPart to a user content item.
How can i weld MyPart based on the content item id?
The issue here is that the content item is not yet created when the constructor is called. I tried hooking into the life cycle with overriding Activating() but that doesn't work either as the content item is also not created yet.
Okay, this task is really difficult. Here's my solution.
1) Create an extension method that welds a content part to a content item (sadly, we cannot use ContentItemBuild.Weld() as there's no chance to pass the content item)
// adopted from ContentItemBuilder.Weld<>()
public static TPart Weld<TPart>(this Orchard.ContentManagement.ContentItem aContentItem)
where TPart: Orchard.ContentManagement.ContentPart, new()
{
var partName = typeof(TPart).Name;
// obtain the type definition for the part
var typePartDefinition = aContentItem.TypeDefinition.Parts.FirstOrDefault(p => p.PartDefinition.Name == partName);
if (typePartDefinition == null) {
// If the content item's type definition does not define the part; use an empty type definition.
typePartDefinition = new Orchard.ContentManagement.MetaData.Models.ContentTypePartDefinition(
new Orchard.ContentManagement.MetaData.Models.ContentPartDefinition(partName),
new Orchard.ContentManagement.MetaData.Models.SettingsDictionary());
}
// build and weld the part
var part = new TPart { TypePartDefinition = typePartDefinition };
aContentItem.Weld(part);
return part;
}
2) Define a StorageFilter for dynamically welding the content part to the content item
public class BaseWeldBeforeStorageFilter<TPart, TRecord> : Orchard.ContentManagement.Handlers.IContentStorageFilter
where TPart: Orchard.ContentManagement.ContentPart, new()
where TRecord: Orchard.ContentManagement.Records.ContentPartRecord
{
// public
public BaseWeldBeforeStorageFilter(Orchard.Data.IRepository<TRecord> aPartRecords)
{
mPartRecords = aPartRecords;
}
...
public void Loading(Orchard.ContentManagement.Handlers.LoadContentContext aContext)
{
// dynamically weld TPart to content item when condition is met (is a user, does record exist)
if (aContext.ContentItem.Is<Orchard.Users.Models.UserPart>())
{
if (!aContext.ContentItem.Is<TPart>())
{
if (mPartRecords.Count(r => r.Id == aContext.ContentItem.Id) > 0)
aContext.ContentItem.Weld<TPart>();
}
}
}
...
// private
Orchard.Data.IRepository<TRecord> mPartRecords;
}
3) Define the content handler for the dynamic content part
public abstract class BasePartHandler<TPart, TRecord> : Orchard.ContentManagement.Handlers.ContentHandler
where TPart: Orchard.ContentManagement.ContentPart<TRecord>, new()
where TRecord: Orchard.ContentManagement.Records.ContentPartRecord, new()
{
// public
// the constructor of a content handler is called when a content item (e.g. user) is created
public BasePartHandler(Orchard.Data.IRepository<TRecord> aPartRecords)
{
...
// add storage filter for dynamically welding TPart to content item
Filters.Add(new BaseWeldBeforeStorageFilter<TPart, TRecord>(aPartRecords));
// enable storing TPart to associated table
Filters.Add(Orchard.ContentManagement.Handlers.StorageFilter.For<TRecord>(aPartRecords));
...
// listen to user creation, update, removal...
OnCreated<Orchard.Users.Models.UserPart>(UserCreated);
...
}
...
// private
private void UserCreated(Orchard.ContentManagement.Handlers.CreateContentContext aContext, Orchard.Users.Models.UserPart aUserPart)
{
if (...) // condition for checking whether user
CreatePartRecordWhenNeededAndWeldPart(aContext.ContentItem, ...);
}
private void CreatePartRecordWhenNeededAndWeldPart(Orchard.ContentManagement.ContentItem aContentItem)
{
TPart lPart = aContentItem.Weld<TPart>();
// assign record, adopted from StorageFilter.cs
// todo: find a way to do it the "Orchard way" as this feels like hack
lPart._record.Loader(r =>
new TRecord {
Id = aContentItem.Id,
ContentItemRecord = new Orchard.ContentManagement.Records.ContentItemRecord {Id = aContentItem.Id}
});
// there are situations where part record already exists in DB but part is not welded at this point, thus check for existing record to avoid
// - creating record multiple times
// - NHibernate exception
if (!mPartRecords.Table.Contains(lPart.Record))
mPartRecords.Create(lPart.Record);
}
private Orchard.Data.IRepository<TRecord> mPartRecords;
}
As for now, the dynamic content part handling is working but I'm still unsure how to create a content part record in Orchard properly (see todo hint in source code of step 3).
I am trying to add a css class if I am in a particular layer.
So 2 questions:
Is it possible to identify the current layer in a Razor view. Something like:
if(currentLayer == "TheHomepage") { ... }
Is the the right way to approach HTML conditional on layer, or is there a better way to do this in Orchard?
If you need to see which layers are currently active, you can do something like this:
#using Orchard.Widgets.Services
#{
var widgetsService = WorkContext.Resolve<IWidgetsService>();
var ruleManager = WorkContext.Resolve<IRuleManager>();
var activeLayerNames = new List<string>();
foreach (var layer in widgetsService.GetLayers()) {
try {
if (ruleManager.Matches(layer.LayerRule)) {
activeLayers.Add(layer.Name);
}
} catch(Exception ex) {
// Problem occurred during layer rule evaluation.
// Just treat it as though the layer rule did not match.
}
}
if (activeLayerNames.Contains("TheHomePage")) {
/* ... Your code here ... */
}
}
Much of the code above makes more sense in a driver or controller, but if you are working only in the view layer, you can do it this way.
You can create a widget that includes the needed #{Style.Include} statements and then add it to a layer.
Follow this instructions to create a new Widget using razor code: Creating simple custom Orchard widgets, name the new widget CssIncluder
Then add this view to your theme, you can use the Shape tracing tool if you like:
Widget-CssIncluder.cshtml:
#{
Model.Metadata.Wrappers.Clear();
Style.Include("somestyle.css");
}
Finally add the widget to the layer of your choice. Be sure to uncheck the title rendering option to get clean code.
Based on Katsuyuki's answer, I created an extension method for WorkContext to convert all active layers into css classes.
using Orchard;
using Orchard.Widgets.Services;
using System.Collections.Generic;
namespace KidsMinistryTeam.Theme.Extensions
{
static public class WorkContextExtensions
{
static public IList<string> GetLayerCssClasses(this WorkContext workContext)
{
var widgetsService = workContext.Resolve<IWidgetsService>();
var ruleManager = workContext.Resolve<IRuleManager>();
var classNames = new List<string>();
foreach (var layer in widgetsService.GetLayers())
{
try
{
if (ruleManager.Matches(layer.LayerRule))
{
classNames.Add(string.Format("{0}-layer", layer.Name.ToLower())); //add any additional class sanitizing logic here
}
}
catch
{
}
}
return classNames;
}
}
}
Then by adding it to Model.Classes in my theme's Layout.cshtml I am now able to style based on active layers.
foreach(string className in WorkContext.GetLayerCssClasses())
{
Model.Classes.Add(className);
}
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)