Orchard Custom Widget with Form - asp.net

I built a custom module that manages appointments for a service-based company. All of the current functionality is contained in the admin section. I have not used a single ContentItem or ContentPart. All the models are just plain records.
I'm looking to create a widget to expose the ability to sign up for an appointment from the front end. I have a partial view and a controller that handles the display and form submit, but I'm not sure how to tie that into a widget that can be placed in one of the content zones of the front-end.
I've spent quite a bit of time researching this, and can't find a good path to follow. (I've tried a few and got sub-optimal results)
Any suggestions?

The best answer for me was to create a widget Type definition in the migration.cs file of the module:
ContentDefinitionManager.AlterTypeDefinition("CreateAppointmentWidget",
cfg => cfg
.WithPart("WidgetPart")
.WithPart("CommonPart")
.WithSetting("Stereotype", "Widget"));
Then create a handler for that widget at /MyModule/Handlers/CreateAppointmentWidgetHandler.cs:
public class CreateAppointmentWidgetHandler : ContentHandler
{
private readonly IRepository<FieldTechRecord> _repository;
public CreateAppointmentWidgetHandler(IRepository<FieldTechRecord> repository)
{
_repository = repository;
}
protected override void BuildDisplayShape(BuildDisplayContext context)
{
base.BuildDisplayShape(context);
if (context.ContentItem.ContentType == "CreateAppointmentWidget")
{
CreateAppointmentViewModel model = new CreateAppointmentViewModel(_repository.Fetch(x => x.IsActive));
context.Shape.AppointmentModel = model;
}
}
}
Then create a matching widget template at /MyModule/Views/Widget-CreateAppointmentWidget.cshtml that inserts the Partial View:
#Html.Partial("CreateAppointment", (MyModule.Models.Views.CreateAppointmentViewModel)Model.AppointmentModel)
The above code grabs the partial view /MyModule/Views/CreateAppointment.cshtml.
Thanks to Giscard's suggestion, I was able to correct the links rendered from CreateAppointment.cshtml by using #Url.RouteUrl() and defining named routes to point where I needed the action and ajax requests to go.
The nice thing about this solution is that it provided a way to create the widget without having to rework my models to use Orchards ContentPart functionality.

Something is not connecting in my head, because I have been able to create a theme with zones, and then dispatch a shape from my module into that zone without much more than doing #Display.Shape(). So I am curious if it's absolutely necessary to use a handler to override the BuildDisplayShape.
Again, this is in the scenario where you have models as plain records (not using ContentItem or ContentPart - and even if not using them, you've shown an example of creating one through migrations).
Something like this - Controller:
public ShapeResult MyShape()
{
var shape = _orchardServices.New.MyPath1_MyShape();
return new ShapeResult(this, shape);
}
Then create a MyShape.cshtml shape with whatever code I have (no need for example).
NOTE: I use a custom IShapeTemplateHarvester file which adds paths where I can store my shapes (instead of using "Views", "Views/Items", "Views/Parts", "Views/Fields", which is the stock in Orchard). It goes something like this:
NB: I hate that code doesn't automatically wrap in SO.
[OrchardSuppressDependency("Orchard.DisplayManagement
.Descriptors.ShapeTemplateStrategy.BasicShapeTemplateHarvester")]
public class MyShapeTemplateHarvester : BasicShapeTemplateHarvester,
IShapeTemplateHarvester
{
public new IEnumerable<string> SubPaths()
{
var paths = base.SubPaths().ToList();
paths.Add("Views/MyPath1");
paths.Add("Views/MyPath2");
return paths;
}
}
Say I have Index.cshtml in my Theme. I have two choices (I use both and use the Theme as the default presentation).
Index.cshtml in Theme folder:
#*Default Content*#
Index.cshtml in Module folder:
#*Special Content overriding Theme's Index.cshtml*#
Display.MyPath1_MyShape()
Even better for me is that I can do this in the Index.cshtml in Theme folder:
#*Whatever content*#
Display.MyPath1_MySecondShape()
There is no ~/MyPath1/MySecondShape.cshtml in the Theme, but there is one in the Module, which the Theme displays! This is great because I can have a special Theme and have multiple modules (that are placed on separate sites) go back and forth with the theme (think Dashboard for different services in the same profession on different sites).
NOTE: The above may only be possible with IThemeSelector implementation such as:
public class MyThemeSelector : IThemeSelector
{
public ThemeSelectorResult GetTheme(RequestContext context)
{
if (MyFilter.IsApplied(context))
{
return new ThemeSelectorResult { Priority = 200,
ThemeName = "MyDashboard" };
}
return null;
}
}
Just my two bits.

Related

Accessing context from a custom View

When configuring the views of a calendar, view specific options can be specified. But the documentation about custom views says nothing on how to retrieve these options.
Is there any way to get these options here and so to make the custom view to behave function of them ?
Is there even a way to access the view object from a custom view callback ? (maybe the options are available on it)
One solution I've used, but I think that should be part of the core behavior, is to use the undocumented viewPropsTransformers option when creating a custom view through a call to createPlugin :
class MorePropsToView {
transform(viewProps, calendarProps) {
return {
...viewProps,
options: calendarProps.viewSpec.optionOverrides,
calendar: calendarProps.calendarApi,
}
}
}
export const myPlugin = createPlugin({
views: {
custom: CustomView,
},
viewPropsTransformers: MorePropsToView,
})
So the two extra props are available in the custom view :
const CustomView = function CustomView({
eventStore,
dateProfile,
options,
calendar,
}) {
console.log(options, calendar)
}

Extending SilverStripe modules extension

I have installed a particular module in my SilverStripe installation. The following is the directory structure
- Root
- framework
- cms
- mymodule
- code
- extensions
- CustomClass.php
Here is an example of CustomClass.php
class CustomClass extends Extension {
public function init() {
}
public function customMethod() {
}
}
I need to override the customMethod method of the CustomClass class. I could easily change this method, but changing here will cause trouble in the future if the modules get updated. All the changes made will be lost.
So for this I want to extend the extension class used in modules.
I have created an extension /mysite/extensions/MyCustomClass.php
class MyCustomClass extends Extension {
public function customMethod() {
//do my code here
}
}
but I have no idea how to apply this. I thought CustomClass::add_extension("MyCustomClass ") but surely this will not work because add_extension method doesn't exist in CustomClass.
How do we cope with this scenario? Can I use Injector instead? If yes, how can it be called in mysite/_config.php instead of _config.yml?
Using injector does solve the problem but have to use _config.yml as well. Here is what I did.
File /mysite/extensions/MyCustomClass.php
class MyCustomClass extends CustomClass {
public function customMethod() {
//do my code here
}
}
in /mysite/_config/config.yml I added following lines
Injector:
CustomClass:
class: MyCustomClass
And in /mysite/_config.php I added following line
$object = Injector::inst()->create('CustomClass');
And it all worked fine.
There is another way to achieve similar functionality without straight up replacing a previous extension. With SilverStripe's extension system, we can control not only what configuration settings are loaded but the order they are loaded. This is important to note because the customMethod function from an extension, it uses the first one it finds from all the extensions loaded.
Because of this, it can be only a matter of controlling when your MyCustomClass extension is loaded so you can have your own customMethod function run.
Let's say the "MyModule" module has the following YAML file defined:
---
Name: MyModuleExtensions
After:
- 'framework/*'
- 'cms/*'
---
Page:
extensions:
- CustomClass
All we need to do is specify a separate YAML file to run before this "MyModule" one. This can be accomplished like:
---
Name: MyCustomModule
Before:
- MyModule/#MyModuleExtensions
---
Page:
extensions:
- MyCustomClass
Now, when you call your customMethod function on whatever class you have your extensions on (so in my example, the Page class), it will call the one from your MyCustomClass extension.

(AngularJS) Only one less file for entire Website

I am a beginner with AngularJS and I have a little problem, I installed grunt-contrib-less to support less files instead css but now I have to declare all less styles that will be compiled into only one css file.
But my problem is normally when I'm using less, I write some code for a specific page, and here I have to write the style code for all pages. This is confusing and not really maintanable so is there a best practice to organize less styles?
I tought that there may be multiple solution:
Apply a class to body tag and change it with I don't know what
(controller, services or other)
(Import LESS file only for one page)
Generate multiple css file depending which style is compiled (but I can't do this because I can't configure grunt correctly)
Do this with DOM manipulation (but it don't find it beautifull because I think Angular must have a good way to solve that problem)
Could you explain me how to have different style for differents views ? I don't want to have the same style for all links in all views and without create hundreds classes I don't know how to do that.
Use directive
and add whatever variables/code/logic you want to add
HTML template(directive) of style can be added to your view and after compile you will get different ui for all your views
for reference read
angular directive
I solve my problem by adding specific class on body tag depending the route.
I put a variable in rootScope called 'pageStyle' with correspond to the classname that I want. This variable is updated automatically when route change (see run function). There is an event when the route change ($stateChangeSuccess or $routeChangeSuccess depending if you are using ngRoute or -angularui-routeur).
In my case i would like to add the name of the route but you can do it with the controller name or something else.
Here is an example
This is the routes :
angular
.module('frontApp', [])
.config(['$stateProvider', '$urlRouterProvider', function ($stateProvider, $urlRouterProvider, $mdThemingProvider) {
$urlRouterProvider.otherwise('/');
$stateProvider
.state('home', {
url: '/',
templateUrl: '../views/home.html',
controller: function ($scope) {
$scope.msg = 'Xavier';
}
})
.state('form', {
url: '/form',
templateUrl: '../views/form.html',
controller: 'FormCtrl'
});
}])
And in the run function you will see the event bound to adapt the class when route change :
.run(function($rootScope) {
$rootScope.pageStyle = '';
// Watch state and set controller name in pageStyle variable when state change
$rootScope.$on('$stateChangeSuccess', function(event, toState) {
event.preventDefault();
if (toState && toState.name && typeof toState.name === 'string'){
$rootScope.pageStyle = toState.name;
} else {
$rootScope.pageStyle = '';
}
});
});
Extra informations :
Note that the event called when route change is different if you are using ngroute. use "$routeChangeSuccess" if you use ngRoute and "$stateChangeSuccess" if you choose to use angular-ui-routeur
If you want to add the controller name instead the route name simply use the follow and replace 'ctrl' with you controller suffixe:
if (toState && toState.controller && typeof toState.controller !== 'function'){
$rootScope.pageStyle = toState.controller.toLowerCase().replace('ctrl','');
}
Hope it help someone else

How to limit Content Types for a given Page Type in Experience Manager

Experience Manager (XPM) (user interface update for SDL Tridion 2011 SP1) lets administrators create Page Types, which have component presentations just like pages, but also add rules on how to create and allow additional content types.
For a given page type, I'd like to simplify an author's options by limiting content type choices.
I understand we can:
Limit the content types, so that for a given page type authors can only create certain predefined content types
In the Dashboard, completely restrict the above to just predefined content types, by un-selecting Content Types Already Used on the Page
Use regions which specify combinations of schemas and templates along with quantity restrictions. Authors can only add (drag-and-drop) certain types and quantity of components to these region. For example, we can output the following on Staging to create a region:
<div>
<!-- Start Region: {
title: "Promos",
allowedComponentTypes: [
{
schema: "tcm:2-42-8",
template: "tcm:2-43-32"
},
],
minOccurs: 1,
maxOccurs: 3
} -->
<!-- place the matching CPs here with template logic (e.g. TemplateBeginRepeat/ TemplateBeginIf with DWT) -->
</div>
Authors may still see components they might want to insert (image below), but won’t be able to add them if regions control what’s allowed
However, folder permissions can reduce what components authors can see/use in the Insert Content library
Did I get them all? Any other ways in XPM functionality or possible extensions to consider on how to limit the allowed content for a given Page Type?
Alvin, you pretty much provided most of the options in your question. Another option, if a custom error message is desired or even finer level of control is to use the Event System. Subscribe to a Page's save event Initiated phase and write some validation code that throws an exception if an unwanted component presentation is on the page.
Since Page Types are really a combination of a page template, any metadata on the page and the types of component presentations on the page, we would need to check that we are dealing with the wanted page type and if encounter an CP that doesn't match what we desire, we can simply throw the exception. Here is some quick code:
[TcmExtension("Page Save Events")]
public class PageSaveEvents : TcmExtension
{
public PageSaveEvents()
{
EventSystem.Subscribe<Page, SaveEventArgs>(ValidateAllowedContentTypes, EventPhases.Initiated);
}
public void ValidateAllowedContentTypes(Page p, SaveEventArgs args, EventPhases phases)
{
if (p.PageTemplate.Title != "My allowed page template" && p.MetadataSchema.Title != "My allowed page metadata schema")
{
if (!ValidateAllowedContentTypes(p))
{
throw new Exception("Content Type not allowed on a page of this type.");
}
}
}
private bool ValidateAllowedContentTypes(Page p)
{
string ALLOWED_SCHEMAS = "My Allowed Schema A; My Allowed Schema B; My Allowed Schema C; etc"; //to-do put these in a parameter schema on the page template
string ALLOWED_COMPONENT_TEMPLATES = "My Allowed Template 1; My Allowed Template 2; My Allowed Template 3; etc"; //to-do put these in a parameter schema on the page template
bool ok = true;
foreach (ComponentPresentation cp in p.ComponentPresentations)
{
if (!(ALLOWED_SCHEMAS.Contains(cp.Component.Schema.Title) && ALLOWED_COMPONENT_TEMPLATES.Contains(cp.ComponentTemplate.Title)))
{
ok = false;
break;
}
}
return ok;
}
}

MVC3 Routing Working 1 Direction Only

I am working on an ASP.NET mvc 3 site that contains several project entities, and then each project has several associated subpages, each that works with a component of the project.
So for instance, I could have a project with several photos, milestones, user info, etc. I have a Project Index view, as well as a Project Home which links to several component pages. Most of the components have two views, Index, and Edit/View.
So I set up a route for the edits and views. Note that my route is in an AREA called ProjectManagement
context.MapRoute(
"ProjectManagement_ProjectPageSingle",
"ProjectManagement/{controller}/{action}/{projectNumber}/{projectChildId}",
new { controller = "Project", action = "Home" }
);
and my controller actions all look similar to this:
public ActionResult Edit(string projectNumber, string projectChildId)
This works well and good when I type in the URL directly in the browser. For instance:
~/ProjectManagement/Milestone/Edit/39999P110175/1
however, when I generate an action link using:
<a href="#Url.Action("Edit", new { projectNumber = Model.Project.ProjectNumber, projectChildId = entry.Id})">
the action URL ends up looking like this:
~/ProjectManagement/Milestone/Edit/39999P110175?projectChildId=1
So the route sorta works...but the action link generator doesn't? Not sure where to go from here. Any advice would be much appreciated.
Note that the same thing occurs while using #Html.ActionLink instead of #Url.Action.
Thanks!
You don't seem to have specified the area name:
#Url.Action(
"Edit",
new {
projectNumber = Model.Project.ProjectNumber,
projectChildId = entry.Id,
area = "ProjectManagement"
}
)
Also make sure that there aren't any other routes that might conflict with this one in your area registration which should look like this:
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"ProjectManagement_default",
"ProjectManagement/{controller}/{action}/{projectNumber}/{projectChildId}",
new { controller = "Project", action = "Home" }
);
}
Thanks to Michael for the response. Here's the answer for others.
I had:
context.MapRoute(
"ProjectManagement_ProjectPage",
"ProjectManagement/{controller}/{action}/{projectNumber}",
new { controller = "Project", action = "Home"}
);
///more routes
context.MapRoute(
"ProjectManagement_ProjectPageSingle",
"ProjectManagement/{controller}/{action}/{projectNumber}/{projectChildId}",
new { controller = "Project", action = "Home" }
);
The URL matched the ProjectPage route as well, so it took that one first. Had to flip the order, so the more specific route came first.
Thanks.

Resources