I have created my navigation menus in Orchard using a mix of Content Item and Custom Link Elements (parts of the website are outside the scope of the CMS). Now there are a couple of links that I need to open in a new window/tab, basically the target="_blank" behaviour.
SInce the original Custom Link does not have any parameters I tried to create an extended version of it. In the admin backend I went to "Content definition" looked up Custom Link and tried to create a copy of it, then add a target field that I could check for and use in my theme's Menu.cshtml file.
However I can't even get the basic carbon copy of the Custom Link item working. It has the same stereotype, same Parts, same Forms (none) as the original Custom Link, and it does appear in the list of items on the admin -> navigation window. However the item does not have a field for the URL/link. It only has the field for Menu Text, nothing else.
So my question is 2-tiered:
How can I get a carbon copy of the Custom Link item type working in my Orchard backend navigation?
When I have my copy of the Custom Link working and add a text field named target, how can I access its value in the Menu.cshtml view?
(I tried simply adding a URL field to my copy, that would then show up in the navigation editor, however the navigation itself would ignore it in the output and create a link to the content item id instead).
Any help is greatly appreciated!
Edit: Here are some screenshots to better illustrate the problem, maybe they can help pin down the problem.
Seems like you've done everything right. Please double check if MenuItemPart is there. This part is responsible for holding the URL information and displaying an editor for it. Not sure if this part is attachable though - if it's not, then make it so in the Content Definition\Parts pane.
Instead of hardwiring things inside Menu.cshtml, you should create a file named MenuItemLink-[YourTypeName].cshtml. This shape file will be used to display your custom menu items. Then you can access any fields via Model.Content object, eg. Model.Content.YourTypeName.FieldWithTargetName.Value.
You need to use the MenuItemPart because it has a few important functions integrated into Orchard.Core.
This works fine:
AdvancedMenuItemPartRecord:
public class AdvancedMenuItemPartRecord : ContentPartRecord
{
public virtual string Target { get; set; }
public virtual string Classes { get; set; }
}
AdvancedMenuItemPart:
public class AdvancedMenuItemPart : ContentPart<AdvancedMenuItemPartRecord>
{
public string Target
{
get { return Retrieve(x => x.Target); }
set { Store(x => x.Target, value); }
}
public string Classes
{
get { return Retrieve(x => x.Classes); }
set { Store(x => x.Classes, value); }
}
}
AdvancedMenuItemPartDriver:
public class AdvancedMenuItemPartDriver : ContentPartDriver<AdvancedMenuItemPart>
{
protected override string Prefix
{
get { return "AdvancedMenuItem"; }
}
protected override DriverResult Editor(AdvancedMenuItemPart part, dynamic shapeHelper)
{
return ContentShape("Parts_AdvancedMenuItem_Edit", () => shapeHelper.EditorTemplate(TemplateName: "Parts/AdvancedMenuItem", Model: part, Prefix: Prefix));
}
protected override DriverResult Editor(AdvancedMenuItemPart part, IUpdateModel updater, dynamic shapeHelper)
{
updater.TryUpdateModel(part, Prefix, null, null);
return Editor(part, shapeHelper);
}
}
AdvancedMenuItemPartHandler (ActivatingFilter add MenuItemPart to your AdvancedMenuItem dynamicaly):
public class AdvancedMenuItemPartHandler : ContentHandler
{
public AdvancedMenuItemPartHandler(IRepository<AdvancedMenuItemPartRecord> repository)
{
Filters.Add(StorageFilter.For(repository));
Filters.Add(new ActivatingFilter<MenuItemPart>("AdvancedMenuItem"));
}
}
Placement.info:
<Place Parts_AdvancedMenuItem_Edit="Content:11"/>
Migrations:
public int UpdateFrom2()
{
SchemaBuilder.CreateTable("AdvancedMenuItemPartRecord",
table => table
.ContentPartRecord()
.Column<string>("Target")
.Column<string>("Classes")
);
ContentDefinitionManager.AlterPartDefinition("AdvancedMenuItemPart", part => part
.WithDescription(""));
ContentDefinitionManager.AlterTypeDefinition("AdvancedMenuItem", cfg => cfg
.WithPart("AdvancedMenuItemPart")
.WithPart("MenuPart")
.WithPart("CommonPart")
.WithIdentity()
.DisplayedAs("Custom Link Advanced")
.WithSetting("Description", "Custom Link with target and classes fields")
.WithSetting("Stereotype", "MenuItem")
// We don't want our menu items to be draftable
.Draftable(false)
// We don't want the user to be able to create new ActionLink items outside of the context of a menu
.Creatable(false)
);
return 3;
}
MenuItemLink-AdvancedMenuItem.cshtml:
#{
var advancedPart = Model.Content.AdvancedMenuItemPart;
var tag = new TagBuilder("a");
tag.InnerHtml = WebUtility.HtmlDecode(Model.Text.Text);
tag.MergeAttribute("href", Model.Href);
if (!string.IsNullOrWhiteSpace(advancedPart.Target)) {
tag.MergeAttribute("target", advancedPart.Target);
}
if (!string.IsNullOrWhiteSpace(advancedPart.Classes))
{
tag.AddCssClass(advancedPart.Classes);
}
}
#Html.Raw(tag.ToString(TagRenderMode.Normal))
Related
I use Orchard CMS 1.10.1. I have created a content type, now I want to have a section menu in admin page to have specific links for this content type. I want it include New link and content item list for this content type.
How can i achieve this? preferably with no editing in the code.
You can't do this without code, at the same time you will need a small code chunk to achieve this, like the following code:
public class AdminMenu : INavigationProvider {
public Localizer T { get; set; }
public string MenuName {
get { return "admin"; }
}
public void GetNavigation(NavigationBuilder builder) {
builder
.Add(T("Your Content Type Display Name"), "1", menu => menu
.Action("List", "Admin", new { area = "Contents", id = "YourContentTypeName" }));
}
}
I have built a custom module in Orchard that creates a new part, type and a custom activity but I'm struggling with the last part of what I need to do which is to create a copy of all the content items associated with a specific parent item.
For instance, when someone creates a "Trade Show" (new type from my module), various subpages can be created off of it (directions, vendor maps, etc.) since the client runs a single show at a time. What I need to do is, when they create a new Trade Show, I want to get the most recent prior show (which I'm doing via _contentManager.HqlQuery().ForType("TradeShow").ForVersion(VersionOptions.Latest).ForVersion(VersionOptions.Published).List().Last() (positive that's not the most efficient way, but it works and the record count would be ~10 after five years), then find all of those child pages that correlate to that old show and copy them into new Content Items. They have to be a copy because on occasion they may have to refer back to parts with the old shows, or it could change, etc. All the usual reasons.
How do I go about finding all of the content items that reference that prior show in an Activity? Here is my full class for the Activity:
using System;
using System.Collections.Generic;
using System.Linq;
using Orchard.Autoroute.Services;
using Orchard.ContentManagement;
using Orchard.Localization;
using Orchard.Projections.Models;
using Orchard.Projections.Services;
using Orchard.Workflows.Models;
using Orchard.Workflows.Services;
using Orchard.Workflows.Activities;
namespace Orchard.Web.Modules.TradeShows.Activities
{
public class TradeShowPublishedActivity : Task
{
private readonly IContentManager _contentManager;
private readonly IAutorouteService _autorouteService;
private readonly IProjectionManager _projectionManager;
public TradeShowPublishedActivity(IContentManager contentManager, IAutorouteService autorouteService, IProjectionManager projectionManager)
{
_contentManager = contentManager;
_autorouteService = autorouteService;
_projectionManager = projectionManager;
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public override LocalizedString Category
{
get { return T("Flow"); }
}
public override LocalizedString Description
{
get { return T("Handles the automatic creation of content pages for the new show."); }
}
public override string Name
{
get { return "TradeShowPublished"; }
}
public override string Form
{
get { return null; }
}
public override IEnumerable<LocalizedString> GetPossibleOutcomes(WorkflowContext workflowContext, ActivityContext activityContext)
{
yield return T("Done");
}
public override IEnumerable<LocalizedString> Execute(WorkflowContext workflowContext, ActivityContext activityContext)
{
var priorShow = _contentManager.HqlQuery().ForType("TradeShow").ForVersion(VersionOptions.Latest).ForVersion(VersionOptions.Published).List().Last();
var tradeShowPart = priorShow.Parts.Where(p => p.PartDefinition.Name == "TradeShowContentPart").Single();
//new show alias
//workflowContext.Content.ContentItem.As<Orchard.Autoroute.Models.AutoroutePart>().DisplayAlias
yield return T("Done");
}
}
}
My Migrations.cs file sets up the part that is used for child pages to reference the parent show like this:
ContentDefinitionManager.AlterPartDefinition("AssociatedTradeShowPart", builder => builder.WithField("Trade Show", cfg => cfg.OfType("ContentPickerField")
.WithDisplayName("Trade Show")
.WithSetting("ContentPickerFieldSettings.Attachable", "true")
.WithSetting("ContentPickerFieldSettings.Description", "Select the trade show this item is for.")
.WithSetting("ContentPickerFieldSettings.Required", "true")
.WithSetting("ContentPickerFieldSettings.DisplayedContentTypes", "TradeShow")
.WithSetting("ContentPickerFieldSettings.Multiple", "false")
.WithSetting("ContentPickerFieldSettings.ShowContentTab", "true")));
Then, my child pages (only one for now, but plenty more coming) are created like this:
ContentDefinitionManager.AlterTypeDefinition("ShowDirections", cfg => cfg.DisplayedAs("Show Directions")
.WithPart("AutoroutePart", builder => builder.WithSetting("AutorouteSettings.AllowCustomPattern", "true")
.WithSetting("AutorouteSettings.AutomaticAdjustmentOnEdit", "false")
.WithSetting("AutorouteSettings.PatternDefinitions", "[{Name:'Title', Pattern: '{Content.Slug}', Description: 'international-trade-show'}]")
.WithSetting("AutorouteSettings.DefaultPatternIndex", "0"))
.WithPart("CommonPart", builder => builder.WithSetting("DateEditorSettings.ShowDateEditor", "false"))
.WithPart("PublishLaterPart")
.WithPart("TitlePart")
.WithPart("AssociatedTradeShowPart") /* allows linking to parent show */
.WithPart("ContainablePart", builder => builder.WithSetting("ContainablePartSettings.ShowContainerPicker", "true"))
.WithPart("BodyPart"));
So you have the Trade Show content item, the next step will be to find all parts with a ContentPickerField, then filter that list down to those where the field contains your show's ID.
var items = _contentManager.Query().List().ToList() // Select all content items
.Select(p => (p.Parts
// Select all parts on content items
.Where(f => f.Fields.Where(d =>
d.FieldDefinition.Name == typeof(ContentPickerField).Name &&
// See if any of the fields are ContentPickerFields
(d as ContentPickerField).Ids.ToList().Contains(priorShow.Id)).Any())));
// That field contains the Id of the show
This could get expensive depending on how many content items are in your database.
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)
Setup:
I am creating a module so that my clients can manage their own corporate emails as content.
The idea is to avoid putting people's real email addresses on a public website, so for a website user to send an email, I get Orchard to display a form. No problem with that. My problem (see below) is related to how Orchard displays content items in the dashboard, not the public facing part of the website.
Moving on:
I have created (see migration.cs below) a content type called EmailAddress. It is basically just a content type wrapper around a content part called CorporateEmailPart.
The other relevant bit of my setup is the driver (see CorporateEmailPartDriver.cs below). I have followed Kevin Kuebler's videos on PluralSight.com to write the driver. Debugging shows it working fine.
The shapes are placed using the Placement.info file in my module.
So, everything would be working fine if it wasn't for...
The Problem:
The driver correctly allows me to create the shape for the editor of my content type, which displays fine.
Or rather, displays fine to allow me to create a NEW EmailAddress. I can save the CorporateEmailPart to the database just fine.
However, when I save the new EmailAddress content type, or try to edit an existing one, the fields for the CorporateEmailPart don't display at all on my EmailAddress editor.
Ie, when in my POST DriverResult Editor method I return the GET Editor ContentShape, only the CommonPart of my Content Type is displayed (ie, the owner field of the ContentItem). No CorporateEmailPart data is displayed. Not even empty text boxes.
I must be missing something simple, cos everything else works.
But I just can't see what...!
CODE:
Migrations.cs:
using Orchard.ContentManagement.MetaData;
using Orchard.Core.Contents.Extensions;
using Orchard.Data;
using Orchard.Data.Migration;
using Wingspan.CorporateEmails.Models;
namespace Wingspan.CorporateEmails {
public class Migrations : DataMigrationImpl {
public int Create() {
ContentDefinitionManager.AlterTypeDefinition("EmailAddress", builder =>
builder
.WithPart("CommonPart")
.Creatable());
SchemaBuilder.CreateTable("CorporateEmailPartRecord", table =>
table.ContentPartRecord()
.Column<string>("Alias")
.Column<string>("EmailAddress")
.Column<int>("DisplayOrder")
.Column<string>("DisplayTitle")
.Column<bool>("IsDefault"));
ContentDefinitionManager.AlterPartDefinition(typeof(CorporateEmailPart).Name, p => p.Attachable());
ContentDefinitionManager.AlterTypeDefinition("EmailAddress", builder =>
builder
.WithPart(typeof(CorporateEmailPart).Name));
SchemaBuilder.CreateTable("EmailMessageRecord", table =>
table
.Column<int>("Id", col => col.PrimaryKey().Identity())
.Column<int>("CorporateEmailPartRecord_Id")
.Column<string>("Sender")
.Column<string>("Recipient")
.Column<string>("Subject")
.Column<string>("Body")
.Column<string>("TitleAndName")
.Column<string>("Address")
.Column<string>("Telephones"));
return 1;
}
}
}
CorporateEmailDriver.cs
using System.Collections.Generic;
using System.Web.Routing;
using Orchard;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using Orchard.Data;
using Orchard.Services;
using Wingspan.CorporateEmails.Models;
using Wingspan.CorporateEmails.ViewModels;
using System.Linq;
namespace Wingspan.CorporateEmails.Drivers {
public class CorporateEmailPartDriver : ContentPartDriver<CorporateEmailPart>{
private readonly IRepository<EmailMessageRecord> _repositoryEmailMessage;
private readonly IEnumerable<IHtmlFilter> _htmlFilters;
private readonly RequestContext _requestContext;
private const string TemplateName = "Parts/CorporateEmail";
protected override string Prefix
{
get
{
{ return "CorporateEmail"; }
}
}
public IOrchardServices Services { get; set; }
public CorporateEmailPartDriver(IRepository<EmailMessageRecord> repositoryEmailMessage)
{
_repositoryEmailMessage = repositoryEmailMessage;
}
public CorporateEmailPartDriver(IOrchardServices services, IEnumerable<IHtmlFilter> htmlFilters, RequestContext requestContext) {
_htmlFilters = htmlFilters;
Services = services;
_requestContext = requestContext;
}
protected override DriverResult Display(CorporateEmailPart part, string displayType, dynamic shapeHelper)
{
return ContentShape(TemplateName, () => shapeHelper.Parts_CorporateEmail(CorporateEmailPart: part));
}
//GET
protected override DriverResult Editor(CorporateEmailPart part, dynamic shapeHelper)
{
var model = BuildEditorViewModel(part);
return ContentShape("Parts_CorporateEmail_Edit", () =>
shapeHelper.EditorTemplate(TemplateName: TemplateName, Model: model, Prefix: Prefix));
}
//POST
protected override DriverResult Editor(CorporateEmailPart part, IUpdateModel updater, dynamic shapeHelper)
{
updater.TryUpdateModel(part, Prefix, null, null);
var model = BuildEditorViewModel(part);
return ContentShape("Parts_CorporateEmail_Edit",
() => shapeHelper.EditorTemplate(TemplateName: TemplateName, Model: model, Prefix: Prefix));
}
#region Private Utilities
private CorporateEmailEditorViewModel BuildEditorViewModel(CorporateEmailPart part)
{
return new CorporateEmailEditorViewModel
{
Alias = part.Alias,
EmailAddress = part.EmailAddress,
DisplayOrder = part.DisplayOrder,
DisplayTitle = part.DisplayTitle,
IsDefault = part.IsDefault,
EmailMessageSummaries = part.EmailMessages.Select(ue => ue.Summary).ToList()
};
}
#endregion
}
}
Any suggestions much appreciated.
OK, a more careful debugging session with a clear mind and I found the issue:
An error is caused when building the viewmodel in my driver:
private CorporateEmailEditorViewModel BuildEditorViewModel(CorporateEmailPart part)
{
return new CorporateEmailEditorViewModel
{
Alias = part.Alias,
EmailAddress = part.EmailAddress,
DisplayOrder = part.DisplayOrder,
DisplayTitle = part.DisplayTitle,
IsDefault = part.IsDefault,
EmailMessageSummaries = part.EmailMessages.Select(ue => ue.Summary).ToList()
};
}
The offending line is:
EmailMessageSummaries = part.EmailMessages.Select(ue => ue.Summary).ToList()
The reason is that there is some problem with retrieving data from the EmailMessageRecord table using the CorporateEmailPartRecord_Id foreign key. I still haven't figured out what (have never used nHibernate which is what orchard uses), but the existence of this error answers the current question.
A Word of Advice:
Orchard works fine.
In fact, it works really well.
What doesn'twork is the code in your module.
So be more careful with your debugging.
Kevin Kuebler's videos are a great orchard learning resource.
When you refactor the name of something, double check to make sure they have ALL been changed.
Addendum:
I have found what was causing the hHibernate error: shoddy refactoring.
I originally called the project CompanyEmails, but then refactored everything to call it CorporateEmails. The only problem being, I didn't refactor everything, did I? There was still a CompanyEmailPartRecord_Id field on the EmailMessageRecord model class, whereas in the database, I had changed it to CorporateEmailRecordPart_Id. So, just another bug, just a failure to refactor properly.
I've written the following simple command inside my module. The Faq type has one custom part with a single field, and one BodyPart. After _cm.Create(item) is run, the item has an Id assigned but I can't find any trace of it in the database and it doesn't appear in Orchard's content tab. Why does the item get an Id but isn't found in the database? And does it need a driver, view, and placement info before it appears in the content tab?
public class ApiCommands : DefaultOrchardCommandHandler
{
private readonly IContentManager _cm;
public ApiCommands(IContentManager cm)
{
_cm = cm;
}
[CommandName("api seed")]
public void Seed()
{
var item = _cm.New("Faq");
item.As<FaqPart>().Question = "Why is the sky blue?";
item.As<BodyPart>().Text = "Shut up and do your homework.";
_cm.Create(item);
}
}
My custom part has no driver this is the Handler:
public FaqHandler(IRepository<FaqPartRecord> repository)
{
Filters.Add(StorageFilter.For(repository));
}
It turns out my type didn't attach a CommonPart. After I attached one and set the Owner property of the part, I was able to save it.