How to create a custom container element for Orchard Layout? - orchardcms

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.

Related

Copy From excel spread sheet into blazor app

I have a hosted Blazor WebAssembly application.
I need a strategy or a sample on how can I copy values from an excel spreadsheet and paste them into the application with a final goal to add them into my database through the existing API.
So the question here is this: what components should I paste the values into, and how should I handle the whole process:
excel > clipboard > Component > save in db
It was actually more difficult than I initially thought. I've created a repo. The result is this.
You can select any elements in Excel, copy them, focus the content of your Blazor page and paste it. As a simple view, it is displayed in a table.
Let's go through the solution.
Index.razor
#page "/"
<div class="form-group">
<label for="parser">Parser type</label>
<select class="form-control" id="parser" #bind="_parserType">
<option value="text">Text</option>
<option value="html">HTML</option>
</select>
</div>
<PasteAwareComponent OnContentPasted="FillTable">
#if (_excelContent.Any() == false)
{
<p>No Content</p>
}
else
{
<table class="table table-striped">
#foreach (var row in _excelContent)
{
<tr>
#foreach (var cell in row)
{
<td>#cell</td>
}
</tr>
}
</table>
}
</PasteAwareComponent>
<button type="button" class="btn btn-primary" #onclick="#( () => _excelContent = new List<String[]>() )">Clear</button>
#code
{
private IList<String[]> _excelContent = new List<String[]>();
...more content, explained later...
}
If you copy a selection from Excel into the clipboard, not a single text is copied, but multiple representations of the same content. In my experiment, it has been three different types.
I've built two different parser: ExcelHtmlContentParser and ExcelTextContentParser. Regarding the many different possibilities of what a cell content in Excel can be, my implementation is merely completed and should be seen as an inspiration. To see both parsers in action, you can choose between them by changing the value in the select box.
The PasteAwareComponent handles the interaction with Javascript. You can place any content inside this component. If this component (or any child) has focus, the paste event will be handled correctly.
<span #ref="_reference">
#ChildContent
</span>
#code {
private ElementReference _reference;
[Parameter]
public RenderFragment ChildContent { get; set; }
[Parameter]
public EventCallback<IEnumerable<IDictionary<String, String>>> OnContentPasted { get; set; }
[JSInvokable("Pasted")]
public async void raisePasteEvent(IEnumerable<IDictionary<String, String>> items)
{
await OnContentPasted.InvokeAsync(items);
}
}
The component handles the interoperation with javascript. As soon the paste events happen the EventCallback<IEnumerable<IDictionary<String, String>>> OnContentPasted is fired.
Potentially, there could be more than one element inside the clipboard. Hence, we need to handle a collection IEnumerable<>. As seen in the picture before, the same clipboard item can have multiple representations. Each representation has a mime-type like "text/plain" or "text/html" and the value. This is represented by the IDictionary<String, String> where the key is the mime-type, and the value is the content.
Before going into the details about the javascript interop, we go back to the Index component.
<PasteAwareComponent OnContentPasted="FillTable">
...
</PasteAwareComponent>
#code {
private async Task FillTable(IEnumerable<IDictionary<String, String>> content)
{
if (content == null || content.Count() != 1)
{
return;
}
var clipboardContent = content.ElementAt(0);
IExcelContentParser parser = null;
switch (_parserType)
{
case "text":
parser = new ExcelTextContentParser();
break;
case "html":
parser = new ExcelHtmlContentParser();
break;
default:
break;
}
foreach (var item in clipboardContent)
{
if (parser.CanParse(item.Key) == false)
{
continue;
}
_excelContent = await parser.GetRows(item.Value);
}
}
}
The index component uses this event callback in the method FillTable. The method checks if there is one element in the clipboard. Based on the selection, the parser is chosen. Each representation is checked in the next step if the chosen parser can parse it, based on the provided mime-type. If the right parser is found, the parser does its magic, and the content of the field _excelContent is updated. Because it is an EventCallback StateHasChanged is called internally, and the view is updated.
The text parser
In the text representation, Excel uses \r\n as the end of the row and a \t for each cell, even the empty ones. The parser logic is quite simple.
public class ExcelTextContentParser : IExcelContentParser
{
public String ValidMimeType { get; } = "text/plain";
public Task<IList<String[]>> GetRows(String input) =>
Task.FromResult<IList<String[]>>(input.Split("\r\n", StringSplitOptions.RemoveEmptyEntries).Select(x =>
x.Split("\t").Select(y => y ?? String.Empty).ToArray()
).ToList());
}
I haven't tested how this behavior changes if the content is more complex. I guess that the HTML representation is more stable. Hence, the second parser.
The HTML parser
The HTML representation is a table. With <tr> and <td>. I've used the library AngleSharp as HTML parser.
public class ExcelHtmlContentParser : IExcelContentParser
{
public String ValidMimeType { get; } = "text/html";
public async Task<IList<String[]>> GetRows(String input)
{
var context = BrowsingContext.New(Configuration.Default);
var document = await context.OpenAsync(reg => reg.Content(input));
var element = document.QuerySelector<IHtmlTableElement>("table");
var result = element.Rows.Select(x => x.Cells.Select(y => y.TextContent).ToArray()).ToList();
return result;
}
}
We are loading the clipboard content as an HTML document, getting the table and iterating over all rows, and selected each column.
** The js interop ***
#inject IJSRuntime runtime
#implements IDisposable
<span #ref="_reference">
#ChildContent
</span>
#code {
private ElementReference _reference;
private DotNetObjectReference<PasteAwareComponent> _objectReference;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender == true)
{
_objectReference = DotNetObjectReference.Create(this);
await runtime.InvokeVoidAsync("BlazorClipboadInterop.ListeningForPasteEvents", new Object[] { _reference, _objectReference });
}
}
public void Dispose()
{
GC.SuppressFinalize(this);
if (_objectReference != null)
{
_objectReference.Dispose();
}
}
}
The PasteAwareComponent component overrides the OnAfterRenderAsync life cycle, to invoke a js interop method. It has to be the OnAfterRenderAsync because before, the HTML reference wouldn't exist, and we need the reference to add the paste event listener. When the paste event occurred the javascript has to call this object, so we need to create a DotNetObjectReference instance. We implemented the IDisposable interface and disposing the reference correctly to prevent memory leaks.
The last part is the javascript part itself. I've created a file called clipboard-interop.js and placed it inside the wwwroot/js folder.
var BlazorClipboadInterop = BlazorClipboadInterop || {};
BlazorClipboadInterop.ListeningForPasteEvents = function (element, dotNetObject) {
element.addEventListener('paste', function (e) { BlazorClipboadInterop.pasteEvent(e, dotNetObject) });
};
We use the HTML reference to register an event listener for the 'paste' event. In the handling method, we create the object that is passed to the C# method.
BlazorClipboadInterop.pasteEvent =
async function (e, dotNetObject) {
var data = await navigator.clipboard.read();
var items = []; //is passed to C#
for (let i = 0; i < data.length; i++) {
var item = {};
items.push(item);
for (let j = 0; j < data[i].types.length; j++) {
const type = data[i].types[j];
const blob = await data[i].getType(type);
if (blob) {
if (type.startsWith("text") == true) {
const content = await blob.text();
item[type] = content;
}
else {
item[type] = await BlazorClipboadInterop.toBase64(blob);
}
}
}
}
dotNetObject.invokeMethodAsync('Pasted', items);
e.preventDefault();
}
When we are using js interop, we should use objects that are easy to serialize. In the case of a real blob, like an image, it would be based64-encoded string, otherwise just the content.
The solution used the navigator.clipboard capabilities. The user needs to allow it. Hence we see the dialog.

Nesting layouts/views keeping the content variable in Zend Framework 2

I am trying to nest two (or more) views using the following code. I am struggling to find a way to successfully nest these views without losing the final view content and passing it through the $this->content variable within the last layout, as it just returns an empty string.
core/Framework/Mvc/Controller/BaseActionController.php
This is a simple base controller which uses the $frame and $layout variables (so that they can be used within any controller extending this class). The idea is the frame is defined as the page starting with <!DOCTYPE html> and the layout is the HTML which gets displayed in the frame using <?= $this->content; ?>.
namespace Framework\Mvc\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
class BaseActionController extends AbstractActionController
{
protected $frame;
protected $layout;
protected $layouts = array();
public function preDispatch() {...}
public function dispatch() {..}
public function postDispatch()
{
if ($this->frame !== null) {
$this->layouts[] = $this->frame;
}
if ($this->layout !== null) {
$this->layouts[] = $this->layout;
}
foreach ($this->layouts as $layout) {
$view = new ViewModel();
$layoutView = new ViewModel();
$layoutView->setTemplate($layout);
$layoutView->addChild($view);
}
}
}
module/Application/view/layout/frame.phtml
The <?= $this->content; ?> part within this template should echo out the layout.phtml template along with it's own <?= $this->content; ?>.
<?= $this->doctype(); ?>
<html>
<head>
<meta charset="utf-8">
<title>Woohoo, I'm a frame</title>
</head>
<body>
<?= $this->content; ?>
</body>
</html>
module/Application/view/layout/admin/layout.phtml
The $this->content variable should echo out the contents of the module/Users/view/users/test/index.phtml file. At this point, the variable returns an empty string.
<header>
<img class="logo" src="<?= $this->basePath() ?>/img/logo.png" alt="Company">
<nav>
<ul>
<li>Home</li>
<li>About</li>
<li>Contact</li>
</ul>
</nav>
</header>
<section>
<?= $this->content; ?>
</section>
<footer>
<ul>
<li>Copyright</li>
<li>Sitemap</li>
<li>Privacy policy</li>
</ul>
</footer>
module/Users/view/users/test/index.phtml
<h1 class="page__title">Test</h1>
<p class="page__content">The final view</p>
Temporary solution (not very nice to write this in each action)
<?php
namespace Users\Controller;
use Framework\Mvc\Controller\BaseActionController;
use Zend\View\Model\ViewModel;
class TestController extends BaseActionController
{
public function indexAction()
{
$view = new ViewModel();
$view->setTemplate('users/test/index.phtml');
$adminView = new ViewModel();
// This layout is defined in the Application module.config.php file
$adminView->setTemplate('layout/admin');
$adminView->addChild($view);
return $adminView;
}
}
As above, my temporary solution is to choose the template the ViewModel() instance needs, manually. I notice $view->setTemplate(); works but without defining one, $view->getTemplate(); returns an empty string. I am not sure where, in Zend Framework 2, the default template is being defined so I can replicate this within the base controller.
I think the solution I have (temporarily) could work, the only issue being the manual $view->setTemplate('/path/to/my/template.phtml');. If I can replicate how Zend does this, then it should work correctly but I am at a loss passing the $this->content variable into the layout.phtml file with the contents being the final view.
UPDATE:
As suggested by Next Developer, I have added the following:
module/Application/Module.php
<?php
namespace Application;
use Zend\Mvc\ModuleRouteListener;
use Zend\Mvc\MvcEvent;
use Zend\Session\Container;
use Framework\Mvc\View\Http\TemplateInjector;
class Module
{
public function onBootstrap(MvcEvent $e)
{
$app = $e->getApplication();
$request = $app->getRequest();
$response = $app->getResponse();
$eventManager = $app->getEventManager();
$serviceManager = $app->getServiceManager();
$session = new Container('locale');
if (!$session->offsetExists('locale')) {
$session->offsetSet('locale', \Locale::acceptFromHttp($request->getServer('HTTP_ACCEPT_LANGUAGE')));
}
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach($eventManager);
$serviceManager->get('translator')
->setLocale($session->locale)
->setFallbackLocale('en_GB');
$eventManager->getSharedManager()
->attach(
'Zend\Stdlib\DispatchableInterface',
MvcEvent::EVENT_DISPATCH,
new TemplateInjector(),
-80
);
}
public function getConfig()
{
return include __DIR__ . '/config/module.config.php';
}
public function getAutoloaderConfig()
{
return array(
'Zend\Loader\StandardAutoloader' => array(
'namespaces' => array(
__NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
),
),
);
}
}
core/Framework/Mvc/View/Http/TemplateInjector.php
<?php
namespace Framework\Mvc\View\Http;
use Zend\Mvc\MvcEvent;
use Zend\View\Model\ModelInterface as ViewModel;
class TemplateInjector
{
public function __invoke(MvcEvent $event)
{
$model = $event->getResult();
if (!$model instanceof ViewModel) {
return;
}
if ($model->getTemplate()) {
return ;
}
$controller = $event->getTarget();
if (!is_object($controller)) {
return;
}
// #todo: Clear this mess up
$namespace = explode('\\', ltrim(get_class($controller), '\\'));
$controllerClass = array_pop($namespace);
array_pop($namespace);
$moduleName = implode('/', $namespace);
$controller = substr($controllerClass, 0, strlen($controllerClass) - strlen('Controller'));
$action = $event->getRouteMatch()->getParam('action');
$model->setTemplate(strtolower($moduleName.'/'.$controller.'/'.$action.'.phtml'));
}
}
Any changes in the TemplateInjector doesn't seem to change the view, by this time it seems too late. It does however set the template on the view. When making a new instance of $view = new VidewModel(); it uses the template defined in the TemplateInjector class which should allow me to automate the layout process, but the scope of everything being set, it seems too late. I know I can access the controller, the view and the model in the TemplateInjector but no matter how I change the views or add children, it doesn't come out on the front end. If anyone could provide a working example, that would be really helpful.
I think the best would be in your case is to override the default template injector with your own. Take a look at this post http://blog.igorvorobiov.com/2014/10/18/creating-a-custom-template-injector-to-deal-with-sub-namespaces-in-zend-framework-2/. It explains pretty much well how to create and setup your own template injector.
Basically, you need to create an event listener and attach it to the event MvcEvent::EVENT_DISPATCH triggered by the current controller. Inside the event listener you can put the logic which determines a path to the requested template. In your case, you can get your child view model by calling $model->getChildrenByCaptureTo('capture'); and set the template name to it as you want.
The default logic which resolves template names can be found here Zend\Mvc\View\Http\InjectTemplateListener::injectTemplate
UPDATE:
Upon discussion with #Titanium, this solution was found to be the correct one.
I have tried to understand you problem, so here's another solution to it.
Replace the previous template injector code with this one:
class TemplateInjector
{
public function __invoke(MvcEvent $e)
{
$model = $e->getResult();
if (!$model instanceof ViewModel)
{
return;
}
$controller = $e->getTarget();
if (!is_object($controller))
{
return ;
}
if (!$controller instanceof LayoutTemplateProviderInterface)
{
return ;
}
$frameTemplate = $controller->getFrameTemplate();
if ($frameTemplate !== null)
{
$e->getViewModel()->setTemplate($controller->getFrameTemplate());
}
$layoutTemplate = $controller->getLayoutTemplate();
if ($layoutTemplate !== null)
{
$model = $e->getResult();
$layoutModel = new ViewModel();
$layoutModel->setTemplate($controller->getLayoutTemplate());
$layoutModel->addChild($model);
$e->setResult($layoutModel);
}
}
}
Now, you need to define interface which your base controller class should implement in order to tell the system that you want to use custom templates:
interface LayoutTemplateProviderInterface
{
public function getFrameTemplate();
public function getLayoutTemplate();
}
Then in your base controller you should implement the interface like so:
abstract class BaseController extends AbstractActionController implements LayoutTemplateProviderInterface
{
private $frameTemplate = 'layout/layout';
private $layoutTemplate = 'layout/admin';
public function getFrameTemplate()
{
return $this->frameTemplate;
}
public function getLayoutTemplate()
{
return $this->layoutTemplate;
}
protected function setFrameTemplate($name)
{
$this->frameTemplate = $name;
}
protected function setLayoutTemplate($name)
{
$this->layoutTemplate = $name;
}
}
The last thing is to change the priority at which our template injector is getting executed.
$eventManager->getSharedManager()
->attach(
'Zend\Stdlib\DispatchableInterface',
MvcEvent::EVENT_DISPATCH,
new TemplateInjector(),
-91
);
So, our template injector will be executed right after the default one, this allows us to avoid resolving the template name and rely on the default logic.
After all this, your action looks like this:
public function testAction()
{
return new ViewModel();
}
As you can see you don't have to create nesting views here, it will be done automatically by TemplateInjector.
If you need to change frame template name or layout template within an action you can do it like so:
$this->setFrameTemplate("new/template");
$this->setLayoutTemplate("new/template");
Let me know if this solution solves your problem so I can remove the first one to make this post clearer.

Orchard CMS: Conditional CSS Class based on Layer

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);
}

Add wrapper to widgets only in a specific zone

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");
}
}
});
}
}
}

Orchard alternates based on Tag

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)

Resources