crosspost: https://orchard.codeplex.com/discussions/461867
Here's the full scenario:
I have an EventItem content item which has an EventItem part containing all the custom fields (intro text, agenda, etc). I have another part attached to EventItem called tracks. The issue now is I want to add a Zone inside tracks so I can render fields from EventItem onto tracks via placement through the dynamic Zone. I know I could pull the data and render it inside the part, but I want it to be flexible and would prefer using placement/zones.
I know dynamic Zones are easy with alternates, like the one below on my Content-EventItem.Detail alternate:
#Display(Model.ImageArea)
However, my part uses a viewmodel so I need to predefine whatever properties/objects I use. I've read this blog as well but it doesn't work, or maybe I missed something:
http://weblogs.asp.net/bleroy/archive/2011/06/30/so-what-are-zones-really.aspx
Here's my full code (omitted irrelevant parts of code):
Track Part Template
#{
Func<dynamic, dynamic> Zone = x => Display(x);
}
<div id="Overview-Tab" class="track-tab">
#Zone(Model.Overview)
</div>
Track Part Viewmodel
public class DisplayAllViewModel
{
//public Shape Overview { get; set; }
public object Overview { get; set; }
public List<TrackItem> Tracks { get; set; }
}
Placement for EventItem
<Match ContentType="EventItem">
<Match DisplayType="Detail">
<Place
Fields_Sections="Overview:1"
Parts_Tracks="Content:2.5.6" />
</Match>
</Match>
The Sections field doesn't display at all when I use the Overview Zone.
Any piece of advise or information would be highly appreciated. Thanks!
This is not how placement and zones work: zones are defined in the Content shape, not in the child shapes below that.
In fact you're making things more difficult than they need to be. If you want to display a field inside your track part template, you can just do so:
#{
var eventItemPart = Model.ContentItem.EventItem;
if (eventItemPart != null) {
#eventItemPart.NameOfTheField.SomePropertyOfThatField
}
}
Alternatively, you may want to read this article: http://weblogs.asp.net/bleroy/archive/2013/02/13/easy-content-templates-for-orchard-take-2.aspx which will show you how to flatten your template structure while maintaining the ability to display the original shapes.
Just add in the part #Display(WorkContext.Layout.Header)
Related
There are several similar questions that sort of deal with this issue like this one or this one offering a pretty hacky solution. None of the ones out there have a clear satisfactory answer, or an answer at all, or are asking quite the same thing to begin with.
Record
public class MyPartRecord : ContentPartRecord
{
public virtual Boolean Property1 { get; set; }
public virtual string Property2 { get; set; }
}
Part
public class MyPart : ContentPart<MyPartRecord>
{
public Boolean Property1
{
get { return Record.Property1; }
set { Record.Property1 = value; }
}
public string...
}
Migration (generated by codegen)
SchemaBuilder.CreateTable("MyPartRecord", table => table
.ContentPartRecord()
.Column("Property1", DbType.Boolean)
.Column("Property2", DbType.String)
);
ContentDefinitionManager.AlterPartDefinition("MyPart", part => part
.Attachable()
);
Editor template
#model Project.Module.Models.MyPart
<fieldset>
<legend>MyPart</legend>
<!-- Property1 -->
#Html.EditorFor(m => m.Property1)
#Html.LabelFor(m => m.Property1)
...
</fieldset>
This is all taken from the official documentation on writing Content Parts and works fine. However, I want my custom Part to also have a MediaLibraryPickerField. Adding one through a migration is easy enough:
ContentDefinitionManager.AlterPartDefinition("MyPart", part => part
.WithField("Image", field => field
.OfType("MediaLibraryPickerField")
.WithDisplayName("Image")
.WithSetting("MediaLibraryFieldSettings.Required", "False")
)
);
But there are several problems I bump into using this approach.
1) I can't render the field in my template, only use placement to have it show up somewhere above or below the rest of the template, so I can't group it with the properties that it belongs to.
2) Since it's attached to MyPart and not the ContentPart of the Type that MyPart gets attached to, admins can't adjust its settings through the GUI, only remove it (is this a bug or a feature that has yet to be implemented?).
3) I'm unsure how to access the MediaLibraryField in code, since ContentItem.MyPart returns a MyPart object now, so ContentItem.MyPart.Image.FirstMediaUrl no longer works.
How do I get around these issues? Am I missing something obvious here? Is there perhaps a way to add Media as a property to my model instead of using a Field and still have Orchard persist it? I would really like to avoid modifying my HTML and copying code from the official implementation to my custom views.
1) Use placement.info and alternates to customize where you want to render the field
2) You should be able to adjust the settings in Dashboard -> Content Definition -> YourContentType -> Parts -> under the MyPart settings.
You could also attach the field to the type instead (note: it isn't really attached to the type, but to the part with the same name as the type):
Migrations.cs:
ContentDefinitionManager.AlterPartDefinition("MyType", part => part
.WithField("Image", field => field
.OfType("MediaLibraryPickerField")
.WithDisplayName("Image")
.WithSetting("MediaLibraryFieldSettings.Required", "False")
)
);
ContentDefinitionManager.AlterTypeDefinition("MyType", type => type
.WithPart("MyType");
3) You can either use the dynamic notation, or search the field:
// cast to dynamic
var url = ((dynamic)ContentItem).MyPart.Image.FirstMediaUrl
// or search for the field
var field = ContentItem.MyPart.Fields.OfType<MediaLibraryPickerField>().Single(f => f.Name == "Image");
// or ContentItem.MyPart.Fields.Single(f => f.Name == "Image") as MediaLibraryPickerField;
var url = field.FirstMediaUrl;
I have a view that creates one main object (Author) and a list of other objects for it (Books). So the created object can return the books by calling author.Books.ToList() for example.
My problem is that I only want users to be able to set certain attributes of the book (name, date etc.). I do not want them to be able to inject the form with javascript and set the Price of a book.
How do I tell the framework that I want to bind author.Books[all].Name (and date), but want to discard author.Books[all].Price?
I know I could just manually test it in the controller, but I felt like there is a better solution and I just can't quite put my finger on it.
Some code for context:
An inputbox from the View:
<input data-val="true" data-val-required="The CanBeBorrowed field is required." id="books_0__CanBeBorrowed" name="books[0].CanBeBorrowed" type="checkbox" value="true"`>
You can see how adding extra input fields would corrupt the data.
The controller:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "Name,Date,Books")]Author author {...}
(In my project, I have different classes with the same structure. That is why it looks silly creating all the books when creating the author.)
This exactly why we need to use ViewModels,
you can just set in ViewModel
[Editable(false)]
public decimal Price { get; set; }
and also in your
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([AuthorViewModel authorVm)
{
var author = _repository.getById(authorVm.Id);
//update only the fields of author object that user is allowed to update.
author.Name = authorVm.Name;
author.Date = authorVm.Date;
}
you can read more about ViewModels and how to use them
here and here
Good day!
In my Orchard, I have several content types all with my custom part attached. This part defines to what users this content is available. For each logged user there is external service, which defines what content user can or cannot access. Now I need access restriction to apply everywhere where orchard display content lists, this includes results by specific tag from a tag cloud, or results listed from Taxonomy term. I seems can’t find any good way to do it except modifying TaxonomyServices code as well as TagCloud services, to join also my part and filter by it. Is this indeed the only way to do it or there are other solutions? I would like to avoid doing changes to built-in modules if possible but cannot find other way.
Thanks in advance.
I'm currently bumbling around with the same issue. One way I'm currently looking at is to hook into the content manager.
[OrchardSuppressDependency("Orchard.ContentManagement.DefaultContentManager")]
public class ModContentManager : DefaultContentManager, IContentManager
{
//private readonly Lazy<IShapeFactory> _shapeFactory;
private readonly IModAuthContext _modAuthContext;
public ModContentManager(IComponentContext context,
IRepository<ContentTypeRecord> contentTypeRepository,
IRepository<ContentItemRecord> contentItemRepository,
IRepository<ContentItemVersionRecord> contentItemVersionRepository,
IContentDefinitionManager contentDefinitionManager,
ICacheManager cacheManager,
Func<IContentManagerSession> contentManagerSession,
Lazy<IContentDisplay> contentDisplay,
Lazy<ISessionLocator> sessionLocator,
Lazy<IEnumerable<IContentHandler>> handlers,
Lazy<IEnumerable<IIdentityResolverSelector>> identityResolverSelectors,
Lazy<IEnumerable<ISqlStatementProvider>> sqlStatementProviders,
ShellSettings shellSettings,
ISignals signals,
//Lazy<IShapeFactory> shapeFactory,
IModAuthContext modAuthContext)
: base(context,
contentTypeRepository,
contentItemRepository,
contentItemVersionRepository,
contentDefinitionManager,
cacheManager,
contentManagerSession,
contentDisplay,
sessionLocator,
handlers,
identityResolverSelectors,
sqlStatementProviders,
shellSettings,
signals) {
//_shapeFactory = shapeFactory;
_modAuthContext = modAuthContext;
}
public new dynamic BuildDisplay(IContent content, string displayType = "", string groupId = "") {
// So you could do something like...
// var myPart = content.As<MyAuthoPart>();
// if(!myPart.IsUserAuthorized)...
// then display something else or display nothing (I think returning null works for this but
//don't quote me on that. Can always return a random empty shape)
// else return base.BuildDisplay(content, displayType, groupId);
// ever want to display a shape based on the name...
//dynamic shapes = _shapeFactory.Value;
}
}
}
Could also hook into the IAuthorizationServiceEventHandler, which is activated before in the main ItemController and do a check to see if you are rendering a projection or taxonomy list set a value to tell your content manager to perform checks else just let them through. Might help :)
I'm using EF5 code first to generate my database schema, but my new navigation property is being named in an undesirable way in the table. here is the model I'm working with.
public class User
{
[Key]
public long UserId { get; set; }
...
**public virtual ICollection<ImagePermission> KeepThisNavigationName { get; set; }**
}
However, After I've updated my database and examine the table columns, the column is named:
dbo.ImagePermission.User_UserId
And I would like it to be named
dbo.ImagePermission.KeepThisNavigationName_UserId
I believe there is a way to do this using the Fluent API, but after many failed attempts, I can't get the desired outcome.
P.s. The 'ImagePermission' Entity is currently still in development, so I would prefer to drop the migration which creates this table so I can create this column name correctly during the table create, rather than having additional code to update the column name.
Many thanks, Oliver
The correct mapping with Fluent API would be:
modelBuilder.Entity<User>()
.HasMany(u => u.KeepThisNavigationName)
.WithOptional() // or WithRequired()
.Map(m => m.MapKey("KeepThisNavigationName_UserId"));
If you have a navigation property in ImagePermission refering to User you need to use WithOptional(i => i.User) (or WithRequired(i => i.User)) instead of the parameterless version.
Lets say I want a different main image for each page, situated above the page title. Also, I need to place page specific images in the left bar, and page specific text in the right bar. In the right and left bars, I also want layer specific content.
I can't see how I can achieve this without creating a layer for each and every page in the site, but then I end up with a glut of layers that only serve one page which seems too complex.
What am I missing?
If there is a way of doing this using Content parts, it would be great if you can point me at tutorials, blogs, videos to help get my head round the issue.
NOTE:
Sitefinity does this sort of thing well, but I find Orchard much simpler for creating module, as well as the fact that it is MVC which I find much easier.
Orchard is free, I understand (and appreciate) that. Just hoping that as the product evolves this kind of thing will be easier?
In other words, I'm hoping for the best of all worlds...
There is a feature in the works for 1.5 to make that easier, but in the meantime, you can already get this to work quite easily with just a little bit of code. You should first add the fields that you need to your content type. Then, you are going to send them to top-level layout zones using placement. Out of the box, placement only targets local content zones, but this is what we can work around with a bit of code by Pete Hurst, a.k.a. randompete. Here's the code:
ZoneProxyBehavior.cs:
=====================
using System;
using System.Collections.Generic;
using System.Linq;
using ClaySharp;
using ClaySharp.Behaviors;
using Orchard.Environment.Extensions;
namespace Downplay.Origami.ZoneProxy.Shapes {
[OrchardFeature("Downplay.Origami.ZoneProxy")]
public class ZoneProxyBehavior : ClayBehavior {
public IDictionary<string, Func<dynamic>> Proxies { get; set; }
public ZoneProxyBehavior(IDictionary<string, Func<dynamic>> proxies) {
Proxies = proxies;
}
public override object GetMember(Func<object> proceed, object self, string name) {
if (name == "Zones") {
return ClayActivator.CreateInstance(new IClayBehavior[] {
new InterfaceProxyBehavior(),
new ZonesProxyBehavior(()=>proceed(), Proxies, self)
});
}
// Otherwise proceed to other behaviours, including the original ZoneHoldingBehavior
return proceed();
}
public class ZonesProxyBehavior : ClayBehavior {
private readonly Func<dynamic> _zonesActivator;
private readonly IDictionary<string, Func<dynamic>> _proxies;
private object _parent;
public ZonesProxyBehavior(Func<dynamic> zonesActivator, IDictionary<string, Func<dynamic>> proxies, object self) {
_zonesActivator = zonesActivator;
_proxies = proxies;
_parent = self;
}
public override object GetIndex(Func<object> proceed, object self, IEnumerable<object> keys) {
var keyList = keys.ToList();
var count = keyList.Count();
if (count == 1) {
// Here's the new bit
var key = System.Convert.ToString(keyList.Single());
// Check for the proxy symbol
if (key.Contains("#")) {
// Find the proxy!
var split = key.Split('#');
// Access the proxy shape
return _proxies[split[0]]()
// Find the right zone on it
.Zones[split[1]];
}
// Otherwise, defer to the ZonesBehavior activator, which we made available
// This will always return a ZoneOnDemandBehavior for the local shape
return _zonesActivator()[key];
}
return proceed();
}
public override object GetMember(Func<object> proceed, object self, string name) {
// This is rarely called (shape.Zones.ZoneName - normally you'd just use shape.ZoneName)
// But we can handle it easily also by deference to the ZonesBehavior activator
return _zonesActivator()[name];
}
}
}
}
And:
ZoneShapes.cs:
==============
using System;
using System.Collections.Generic;
using Orchard.DisplayManagement.Descriptors;
using Orchard;
using Orchard.Environment.Extensions;
namespace Downplay.Origami.ZoneProxy.Shapes {
[OrchardFeature("Downplay.Origami.ZoneProxy")]
public class ZoneShapes : IShapeTableProvider {
private readonly IWorkContextAccessor _workContextAccessor;
public ZoneShapes(IWorkContextAccessor workContextAccessor) {
_workContextAccessor = workContextAccessor;
}
public void Discover(ShapeTableBuilder builder) {
builder.Describe("Content")
.OnCreating(creating => creating.Behaviors.Add(
new ZoneProxyBehavior(
new Dictionary<string, Func<dynamic>> { { "Layout", () => _workContextAccessor.GetContext().Layout } })));
}
}
}
With this, you will be able to address top-level layout zones using Layout# in front of the zone name you want to address, for example Layout#BeforeContent:1.
ADDENDUM:
I have used Bertrand Le Roy's code (make that Pete Hurst's code) and created a module with it, then added 3 content parts that are all copies of the bodypart in Core/Common.
In the same module I have created a ContentType and added my three custom ContentParts to it, plus autoroute and bodypart and tags, etc, everything to make it just like the Orchard Pages ContentType, only with more Parts, each with their own shape.
I have called my ContentType a View.
So you can now create pages for your site using Views. You then use the ZoneProxy to shunt the custom ContentPart shapes (Parts_MainImage, Parts_RightContent, Parts_LeftContent) into whatever Zones I need them in. And job done.
Not quite Sitefinity, but as Bill would say, Good enough.
The reason you have to create your own ContentParts that copy BodyPart instead of just using a TextField, is that all TextFields have the same Shape, so if you use ZoneProxy to place them, they all end up in the same Zone. Ie, you build the custom ContentParts JUST so that you get the Shapes. Cos it is the shapes that you place with the ZoneProxy code.
Once I have tested this, I will upload it as a module onto the Orchard Gallery. It will be called Wingspan.Views.
I am away on holiday until 12th June 2012, so don't expect it before the end of the month.
But essentially, with Pete Hurst's code, that is how I have solved my problem.
EDIT:
I could have got the same results by just creating the three content parts (LeftContent, RightContent, MainImage, etc), or whatever content parts are needed, and then adding them to the Page content type.
That way, you only add what is needed.
However, there is some advantage in having a standard ContentType that can be just used out of the box.
Using placement (Placement.info file) you could use the MainImage content part for a footer, for example. Ie, the names should probably be part 1, part 2, etc.
None of this would be necessary if there was a way of giving the shape produced by the TextField a custom name. That way, you could add as may TextFields as you liked, and then place them using the ZoneProxy code. I'm not sure if this would be possible.