Orchard CMS: ContentPart not added to ContentItem in migration - orchardcms

I am trying to build a module with the following in a migration:
public class XyzzyPartRecord : ContentPartRecord
{
public virtual string Plugh { get; set; }
}
public class XyzzyPart : ContentPart<XyzzyPartRecord>
{
public string Plugh {
get { return Retrieve( r => r.Plugh ); }
set { Store( r => r.Plugh, value ); }
}
}
public int Create() {
SchemaBuilder.CreateTable( "XyzzyPartRecord", table => table
.ContentPartRecord()
.Column<string>( "Plugh" )
);
ContentDefinitionManager.AlterPartDefinition( "XyzzyPart", cfg => cfg
.WithDescription( "XyzzyPart" ) );
ContentDefinitionManager.AlterTypeDefinition( "XyzzyItem", cfg => cfg
.WithPart( "XyzzyPart" )
);
return 1;
}
When accessing an XyzzyItem, there is no XyzzyPart in the Parts collection. Instead there is a ContentPart.
How do I get my Content Part to allow it to be added to a Content Item's Parts collection?

I had neglected to create either a Driver or a Handler for my part. Once those were in place my code worked as expected.
namespace MyProject.Drivers {
public class XyzzyPartDriver : ContentPartDriver<XyzzyPart> {
protected override DriverResult Display( XyzzyPart part, string displayType, dynamic shapeHelper ) {
return ContentShape( "Parts_Xyzzy",
() => shapeHelper.Parts_Xyzzy(
Xyzzy: part ) );
}
}
}
namespace MyProject.Handlers {
public class XyzzyPartHandler : ContentHandler {
public XyzzyPartHandler( IRepository<XyzzyPartRecord> repository ) {
Filters.Add( StorageFilter.For( repository ) );
}
}
}

Related

Saving data on postback in Orchard

I'm an Orchard newbie and I'm having difficulty trying to get the form data when a new item is created.
What I have is a module that creates a menu item on the admin dashboard. That menu item will load a page where a user can enter a new "Coach".
There are 3 things needed for a coach, first name, last name and email.
Here's the code I have implemented for this...
migrations.cs
public class SDSDataMigration : DataMigrationImpl
{
public int Create()
{
SchemaBuilder..CreateTable("CoachPartRecord", table => table.ContentPartRecord()
.Column("FirstName", DbType.AnsiString, c => c.WithLength(50))
.Column("LastName", DbType.AnsiString, c => c.WithLength(50))
.Column("Email", DbType.AnsiString, c => c.WithLength(200)))
ContentDefinitionManager.AlterPartDefinition("CoachPart", part => part
.WithField("FirstName", f => f.OfType("TextField"))
.WithField("LastName", f => f.OfType("TextField"))
.WithField("Email", f => f.OfType("TextField"))
);
ContentDefinitionManager.AlterTypeDefinition("Coach", type => type.WithPart("CommonPart")
.WithPart("CoachPart"));
return 1;
}
}
parts/records
public class CoachPartRecord : ContentPartRecord
{
public virtual string FirstName { get; set; }
public virtual string LastName { get; set; }
public virtual string Email { get; set; }
}
public class CoachPart : ContentPart<CoachPartRecord>
{
public string FirstName
{
get { return Record.FirstName; }
set { Record.FirstName = value; }
}
public string LastName
{
get { return Record.LastName; }
set { Record.LastName = value; }
}
public string Email
{
get { return Record.Email; }
set { Record.Email = value; }
}
}
view for creating editor
#{ Layout.Title = T("Add Coach").ToString(); }
#using (Html.BeginFormAntiForgeryPost()) {
// Model is a Shape, calling Display() so that it is rendered using the most specific template for its Shape type
#Display(Model)
}
handler
public class CoachPartHandler : ContentHandler
{
public CoachPartHandler(IRepository<CoachPartRecord> repository)
{
Filters.Add(StorageFilter.For(repository));
}
}
driver
protected override DriverResult Editor(CoachPart part, IUpdateModel updater, dynamic shapeHelper)
{
updater.TryUpdateModel(part, Prefix, null, null);
return Editor(part, shapeHelper);
}
controller (for dashboard menu item)
public ActionResult Create()
{
var coach = _services.ContentManager.New("Coach");
var model = _services.ContentManager.BuildEditor(coach);
return View(model);
}
[HttpPost, ActionName("Create")]
public ActionResult CreatePOST()
{
var contentItem = _services.ContentManager.New("Coach");
_services.ContentManager.Publish(contentItem);
return View("Index");
}
Right now I can get the form to appear to create a new coach. When I hit "Publish" all of the fields (i.e. FirstName, LastName, Email) for the CoachPart parameter in the driver are null.
I can look at the http request and I can see the values I put on the form, but they're not making it to the CoachPart.
Any ideas why the CoachPart fields aren't getting filed in?
Thanks!
First of all, you are defining the properties on your own record. Therefore you don't need new textfields attached to your part, so you should remove this:
ContentDefinitionManager.AlterPartDefinition("CoachPart", part => part
.WithField("FirstName", f => f.OfType("TextField"))
.WithField("LastName", f => f.OfType("TextField"))
.WithField("Email", f => f.OfType("TextField")));
Secondly, because you use your custom controller istead of orchard's content controller, you must implement the IUpdateModel and act on it:
[Admin]
public class MyController : Controller, IUpdateModel {
private readonly IContentManager _contentManager;
private readonly ITransactionManager _transactionManager;
public MyController(IContentManager contentManager,
ITransactionManager transactionManager) {
_contentManager = contentManager;
_transactionManager = transactionManager;
}
[HttpPost, ActionName("Create")]
public ActionResult CreatePOST()
{
var contentItem = _contentManager.New<CoachPart>("Coach");
// The implementation of IUpdateModel is necessary for this next line:
var model = _contentManager.UpdateEditor(contentItem, this);
if (!ModelState.IsValid) {
_transactionManager.Cancel();
return View(model);
}
_contentManager.Publish(contentItem);
return View("Index");
}
bool IUpdateModel.TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties) {
return TryUpdateModel(model, prefix, includeProperties, excludeProperties);
}
void IUpdateModel.AddModelError(string key, LocalizedString errorMessage) {
ModelState.AddModelError(key, errorMessage.ToString());
}
}

Add field to Site Settings in Orchard CMS

this question is similar to this one but it targets Orchard CMS 1.8, where Site Settings were redesigned.
I've implemented HomeSettingsPart to store my custom settings for Home page.
public class HomeSettingsPart : ContentPart {
public int NewsCount {
get { return this.Retrieve(x => x.NewsCount); }
set { this.Store(x => x.NewsCount, value); }
}
public int MaterialsCount {
get { return this.Retrieve(x => x.MaterialsCount); }
set { this.Store(x => x.MaterialsCount, value); }
}
public string WelcomeText {
get { return this.Retrieve(x => x.WelcomeText); }
set { this.Store(x => x.WelcomeText, value); }
}
}
Also I've added a handler for this part:
public class HomeSettingsHandler : ContentHandler {
public HomeSettingsHandler() {
T = NullLocalizer.Instance;
Filters.Add(new ActivatingFilter<HomeSettingsPart>("Site"));
Filters.Add(new TemplateFilterForPart<HomeSettingsPart>("HomeSettings", "Parts.HomeSettings", "HomePage"));
}
public Localizer T { get; set; }
protected override void GetItemMetadata(GetContentItemMetadataContext context) {
if (context.ContentItem.ContentType != "Site")
return;
base.GetItemMetadata(context);
context.Metadata.EditorGroupInfo.Add(new GroupInfo(T("HomePage")));
}
}
Everything is like in this tutorial and this code works fine.
Now I want to add an MediaLibraryPickerField to this content part. I've added this field in migrations as usual and I see it in AdminUI ContentManagement/Parts section.
ContentDefinitionManager.AlterPartDefinition("HomeSettingsPart", x => x
.WithField("HomeSlider", y => y
.OfType("MediaLibraryPickerField")
.WithDisplayName("...")
.WithSetting("MediaLibraryPickerFieldSettings.Multiple", "true")
)
);
But, when I open my Settings/HomeSettings I see only 3 editors for my HomeSettingsPart and nothing is rendered for picker field.
So, how do I add this field correctly?
thanks.
You can also see this page in the documentation site : http://docs.orchardproject.net/Documentation/Adding-custom-settings

Problems mapping a type that inherits from IEnumerable

I have a problem mapping a property containing a custom list that inherits from IEnumerable (if i remove that inheritance, this example works). I have simplified the problem into this model:
public interface IMyEnumerable<T> : IEnumerable<T> { }
public class MyIEnumerable<T> : IMyEnumerable<T>
{
private readonly IEnumerable<T> _items;
public MyIEnumerable(IEnumerable<T> items)
{
_items = items;
}
public IEnumerator<T> GetEnumerator()
{
return _items.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
public class Source
{
public List<SourceItem> Items { get; set; }
}
public class Destination
{
public IMyEnumerable<DestinationItem> Items { get; set; }
}
public class SourceItem
{
public string Name { get; set; }
}
public class DestinationItem
{
public string Name { get; set; }
}
Then i try to use is this way:
public class MyResolver : ValueResolver<Source, IMyEnumerable<DestinationItem>>
{
protected override IMyEnumerable<DestinationItem> ResolveCore(Source source)
{
var destinationItems = Mapper.Map<List<SourceItem>, IEnumerable<DestinationItem>>(source.Items);
return new MyIEnumerable<DestinationItem>(destinationItems);
}
}
// Mappings
Mapper.CreateMap<Source, Destination>()
.ForMember(x => x.Items, m => m.ResolveUsing<MyResolver>());
Mapper.CreateMap<SourceItem, DestinationItem>();
// Using the mappings
var source = // not really relevant
var destination = Mapper.Map<Destination>(source);
This gives me the following exception (slightly edited for readability):
Mapping types:
MyIEnumerable`1 -> IMyEnumerable`1
MyIEnumerable`1[[DestinationItem]] -> IMyEnumerable`1[[DestinationItem]]
Destination path:
Destination.Items.Items
Source value:
MyIEnumerable`1[DestinationItem]
----> System.ArgumentException : Object of type System.Collections.Generic.List`1[DestinationItem] cannot be converted to type IMyEnumerable`1[DestinationItem].
Any idea how i can fix the mapping so that i can get this to work?
Assuming the following:
var source = new Source
{
Items = new List<SourceItem>
{
new SourceItem { Name = "foo" },
new SourceItem { Name = "bar" },
new SourceItem { Name = "cow" },
}
};
Then the following work:
// Method 1: Straight up mapping the collections:
Mapper.CreateMap<List<SourceItem>, IMyEnumerable<DestinationItem>>()
.ConstructUsing(list => new MyEnumerable<DestinationItem>(list.ConvertAll(Mapper.Map<SourceItem, DestinationItem>)));
// Method 2: Ignore the property and do it ourselves after the rest of the mapping:
Mapper.CreateMap<Source, Destination>()
.ForMember(q => q.Items, r => r.Ignore())
.AfterMap((s, d) => d.Items = new MyEnumerable<DestinationItem>(
s.Items.Select(Mapper.Map<SourceItem, DestinationItem>)));
Nothing else seems to work due to some combination of covariance and contravariance between List<T>, IEnumerable<T> and IMyEnumerable<T>

Autoroute URL patterns containing custom tokens

I'm having trouble getting my custom tokens to work with my ContentPart. My problem is the same as what is described here:
Is it possible to create an orchard autoroute using contents of a custom type property?
I have created my tokens:
namespace MyNS.Types.Providers
{
public class BioPartTokens : ITokenProvider
{
public BioPartTokens() {
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public void Describe(dynamic context) {
context.For("Bio", T("Bio"), T("Tokens for the Bio content type"))
.Token("FirstName", T("FirstName"), T("First name of person."))
.Token("LastName", T("LastName"), T("Last name of person."));
}
public void Evaluate(dynamic context) {
context.For<BioPart>("Bio")
.Token("FirstName", (Func<BioPart, object>) (f => f.ContentItem.Parts.OfType<BioPart>().First().FirstName.ToLower()))
.Chain("FirstName", "FirstName", (Func<BioPart, object>)(f => f.ContentItem.Parts.OfType<BioPart>().First().FirstName.ToLower()))
.Token("LastName", (Func<BioPart, object>)(f => f.ContentItem.Parts.OfType<BioPart>().First().LastName.ToLower()))
.Chain("LastName", "LastName", (Func<BioPart, object>)(f => f.ContentItem.Parts.OfType<BioPart>().First().LastName.ToLower()))
;
}
}
}
My model:
namespace MyNS.Types.Models
{
public class BioPart: ContentPart<BioPartRecord>
{
public string FirstName {
get { return Record.FirstName; }
set { Record.FirstName = value; }
}
public string LastName
{
get { return Record.LastName; }
set { Record.LastName = value; }
}
public RoleInSchool RoleInSchool
{
get { return Record.RoleInSchool; }
set { Record.RoleInSchool = value; }
}
public bool IsBlogger {
get { return Record.IsBlogger; }
set { Record.IsBlogger = value; }
}
}
}
Though I've tried URL patterns with all of the following tokens, I've not been able to get a value back from the form that I've submitted:
{Content.Bio.FirstName}
{Content.BioPart.FirstName}
{Bio.FirstName}
{BioPart.FirstName}
No errors are being logged.

Automapper - Map array of objects to one single object

I would like map array of objects to one single object.
For example:
public class ArrayData
{
//name of property in class MyObject in upper under score casing
public string PropName{get;set;}
//value of property in class MyObject
public string PropValue{get;set;}
}
Source data:
ArrayData [] sourceData = new ArrayData[]{new ArrayData{PropName="MY_ID",PropValue="1"}}
Destination object:
public class MyObject
{
public int MyId{get;set;}
}
My aim is set MyObject.MyId to 1.
Convention is:
if(ArrayData.PropName == MyObject.Property.Name)
{
MyObject.PropName = ArrayData.PropValue;
}
EDITED: I tried this way:
public class UpperUnderscoreNamingConvention : INamingConvention
{
#region Implementation of INamingConvention
public Regex SplittingExpression
{
get { throw new System.NotImplementedException(); }
}
public string SeparatorCharacter
{
get { return string.Format("_"); }
}
#endregion
}
public class TestProfile: Profile
{
public override string ProfileName { get { return "TestProfile"; } }
protected override void Configure()
{
SourceMemberNamingConvention = new UpperUnderscoreNamingConvention();
DestinationMemberNamingConvention = new PascalCaseNamingConvention();
CreateMap<ArrayData, MyObject>()
.ForMember(dest => dest.MyId, opt =>
{
opt.Condition(src => src.ColumnName == "MY_ID");
opt.MapFrom(src => src.Value);
});
}
}
Conversion:
Mapper.Initialize((obj) => obj.AddProfile(new TestProfile()));
var myClass = Mapper.Map<ArrayData[], MyClass>(sourceData);
It doesnt work, I get this exception:
AutoMapper.AutoMapperMappingException: Missing type map configuration
or unsupported mapping.
Also I think it is not good solution map all properties manualy:
.ForMember(dest => dest.MyId, opt => opt.Condition(src => src.ColumnName =="MY_ID"))

Resources