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!!
Related
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");
}
}
});
}
}
}
My question is related to this one, but instead of changing a question I thought it Would be better to ask a new one.
I've now got a list of IContent items using the _taxonomyService.GetContentItems(term)
as suggested by #Bertrand Le Roy in the question mentioned above
But how do I turn this into a useful Html string, that I can update on the client via an ajax post?
public class HomeController : Controller
{
private readonly IOrchardServices _services;
private readonly IBlogService _blogService;
private readonly IBlogPostService _blogPostService;
private readonly IFeedManager _feedManager;
private readonly IArchiveConstraint _archiveConstraint;
private readonly ITaxonomyService _taxonomyService;
public HomeController(
IOrchardServices services,
IBlogService blogService,
IBlogPostService blogPostService,
IFeedManager feedManager,
IShapeFactory shapeFactory,
IArchiveConstraint archiveConstraint,
ITaxonomyService taxonomyService) {
_services = services;
_blogService = blogService;
_blogPostService = blogPostService;
_feedManager = feedManager;
_archiveConstraint = archiveConstraint;
T = NullLocalizer.Instance;
Shape = shapeFactory;
_taxonomyService = taxonomyService;
}
dynamic Shape { get; set; }
public Localizer T { get; set; }
public ActionResult Index()
{
return View();
}
[HttpPost]
public JsonResult ListByArchive(string path, IEnumerable<string> category)
{
try
{
// get year and month from path
path = path.ToLower().Substring(path.LastIndexOf(#"/archive/", StringComparison.Ordinal) + 9);
var date = path.Split('/');
var month = int.Parse(date[1]);
var year = int.Parse(date[0]);
// get list of terms ids from strings
var taxonomyPart = _taxonomyService.GetTaxonomyByName("Category");
var terms = category.Select(cat => _taxonomyService.GetTermByName(taxonomyPart.Id, cat)).ToList();
// get list of content items by term avoiding duplicates
var posts = new List<IContent>();
foreach (var term in terms)
{
var items = _taxonomyService.GetContentItems(term);
foreach (var item in items)
{
if (!posts.Select(p => p.Id).Contains(item.Id))
{
posts.Add(item);
}
}
}
// filter by date
var byDate = posts.Where(x =>
{
var publishedUtc = x.ContentItem.As<CommonPart>().CreatedUtc;
return
publishedUtc != null
&& publishedUtc.Value.Month == month
&& publishedUtc.Value.Year == year;
});
....
This gets me my list of IContent, but how do I get a the html for the rendered list ?
I've tried
var range = byDate.Select(x => _services.ContentManager.BuildDisplay(x, "Summary"));
var list = Shape.List();
list.AddRange(range);
dynamic viewModel = Shape.ViewModel().ContentItems(list);
var html = View((object)viewModel);
return Json(new { html = html });
but it returns an empty view,
{"html":{"MasterName":"","Model":[],"TempData":[],"View":null,"ViewBag":{},"ViewData":[],"ViewEngineCollection":[{"HostContainer":{}}],"ViewName":""}}
I have a view called ListByArchive.cshtml, that matches the one it the orchard.blog module.
As an aside, I should be returning a partial view result, instead of a jason result, but when I change the Action result type I get a 404. result from the server.
This is never going to work the way you think it does:
var html = View((object)viewModel);
The easiest way to return HTML representing the content item is to:
Mark your action with ThemedAttribute, ie. [Themed(false)]
Return new ShapeResult(this, viewModel) (full view) or new ShapePartialResult(this, viewModel) (partial view) instead of Json(new { html = html })
Rendering a shape/view to string inside the action is also possible, but way more tricky.
EDIT: I assumed you already have /Views/ViewModel.cshtml file in place. Like Bertrand Le Roy noted below - if it's not there, you need to add one to be able to create a shape using Shape.ViewModel().
I've 3 of .NET Projects.
One of these project is an ASP.Net Web Form Application named Index. And the other projects are listed below
Index.Database Project is Database Layer
Index.Platform Project is Bussiness layer.
In bussiness layer im loading UserControls. There is information bout these UserControls in db. (Path, ascx file, name, Title, Content, Positions etc )
In bussiness layer i created a class drived from UserControl named ModuleControl.
Index.Platform referenced by System.Web.dll also uses Using System.Web.UI.WebControls.
I'm planning to use this ModuleControl fields in my loaded UserControls. There is another class named IndexSite instanced on Default.aspx's Load_Page event.
namespace Index.Platform.Modules
{
public class ModuleControl : System.Web.UI.UserControl
{
public string Title { get; set; }
public bool ShowTitle { get; set; }
public string Content { get; set; }
public ModuleControl()
{
}
}
}
//Index.Platform.IndexSite
private void InitializeModules(System.Web.UI.Page page)
{
string mPath = null;
try
{
ModuleDictionaryList = DBFactory.LoadModules();
PositionList = DBFactory.PositionList;
ModuleList = DBFactory.ModuleList;
foreach (KeyValuePair<string, List<module>> pair in ModuleDictionaryList)
{
foreach (var item in pair.Value)
{
mPath = "/Modules/" + item.Name + "/" + item.Name + ".ascx";
iControl = (ModuleControl)page.LoadControl(mPath);
ShowTitle = Convert.ToBoolean(item.ShowTitle);
iControl.ShowTitle = ShowTitle;
iControl.Title = item.Title;
iControl.Content = item.Content;
panel = (PlaceHolder)page.Master.FindControl(item.Position);
panel.Controls.Add(iControl);
//HttpContext.Current.Response.Write(item.Position + "<br>");
}
}
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
}
this class takes an argument from Default.aspx. Page sends its self.
protected void Page_Load(object sender, EventArgs e)
{
IndexSite site = IndexSite.Instance.Create(this);
}
Yes, In Bussines layer I use LoadControl method to load USerControl also im adding these controls to a panel control which is in MasterPage (PlaceHolder).
My problem is in this line: iControl = (ModuleControl)page.LoadControl(mPath);
I cant cast UserControl to ModuleControl. Remember this ModuleControl drived from UserControl and all of my ascx file drived from ModuleControl class.
Throws this error: "Unable to cast object of type 'ASP.modules_mod_login_mod_login_ascx' to type 'Index.Platform.Modules.ModuleControl'."
If i do these proccess in Main application there is no error for casting ModuleControl.
When id seperate my application to 3. I stuck here.
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'm working on Orchard 1.10. The goal is to design a news website based on it. I have a problem that has not been solved.
That is how to generate a recent news list along with a news which I currently view in detail. I mean when I select a news, I need to show other news which has an Id lower than current news's ID along with it.
Any suggests are welcome. Thank you.
You can create a custom part and attach it to the News content type. Something like this:
public class RecentNewsPart : ContentPart {
}
public class RecentNewsPartDriver : ContentPartDriver<RecentNewsPart> {
private readonly IContentManager _contentManager;
public RecentNewsPartDriver(IContentManager contentManager) {
_contentManager = contentManager;
}
protected override DriverResult Display(RecentNewsPart part, string displayType, dynamic shapeHelper) {
return ContentShape("Parts_RecentNewsPart", () => {
// or however the date is stored on your news,
// maybe just the CommonPart CreatedUtc
var currentNewsPart = part.As<NewsPart>();
var currentNewsDate = currentNewsPart.Date;
var recentNews = _contentManager
.Query<NewsPart, NewsPartRecord>("News")
.Where(c => c.Date < currentNewsDate)
.OrderByDescending(c => c.Date)
.Slice(0, 10)
.ToList();
return shapeHelper.Parts_RecentNewsPart(Items: recentNews);
});
}
Placement.info:
<Match ContentType="News">
<Place Parts_RecentNewsPart="/AsideSecond:3" />
</Match>