I have been struggling with what I thought would be simple.
I have a content type called Supplier. This supplier has contact information containing two addresses, one for Correspondence Address and one for Visiting Address. The supplier has also several locations, like location north and location south. A location is also an address. So basically I have a content item Supplier with a lot of addresses and all of them with their own type.
Migration:
public int Create() {
//Creating the Location contentrecord, contentpart and contenttype
SchemaBuilder.CreateTable("LocationPartRecord", table => table
.ContentPartRecord()
.Column<int>("LocationsPartRecord_id")
);
ContentDefinitionManager.AlterPartDefinition("LocationPart", part => part
.Attachable(false)
.WithField("LocationName", f => f.OfType("TextField"))
.WithField("AddressLine1", f => f.OfType("TextField"))
.WithField("AddressLine2", f => f.OfType("TextField"))
.WithField("Zipcode", f => f.OfType("TextField"))
.WithField("City", f => f.OfType("TextField"))
.WithField("Country", f => f.OfType("TextField")));
ContentDefinitionManager.AlterTypeDefinition("Location",
cfg => cfg
.WithPart("CommonPart")
.WithPart("LocationPart")
);
//Creating the Locations 'container' contentpart
SchemaBuilder.CreateTable("LocationsPartRecord", table => table
.ContentPartRecord()
);
ContentDefinitionManager.AlterPartDefinition("LocationsPart", builder => builder.Attachable());
//Creating the supplier. Specific supplier contentfields can be added later. Doing records, so I can add
//datafields later that are not contentfields
SchemaBuilder.CreateTable("SupplierPartRecord", table => table
.ContentPartRecord());
ContentDefinitionManager.AlterPartDefinition("SupplierPart", part => part
.Attachable(false)
);
ContentDefinitionManager.AlterTypeDefinition("Supplier", builder => builder
.Creatable()
.Draftable()
.WithPart("CommonPart")
.WithPart("TitlePart")
.WithPart("BodyPart")
.WithPart("AutoroutePart", partBuilder =>
partBuilder.WithSetting("AutorouteSettings.AllowCustomPattern", "true")
.WithSetting("AutorouteSettings.PatternDefinitions", "[{Name:'Supplier', Pattern: 'aanbieders/{Content.Slug}', Description: 'aanbieders/supplier-name'}]")
.WithSetting("AutorouteSettings.DefaultPatternIndex", "0"))
.WithPart("SupplierPart")
.WithPart("LocationsPart"));
return 1;
}
Models:
*LocationPartRecord and LocationPart *
public class LocationPartRecord:ContentPartRecord {
public virtual LocationsPartRecord LocationsPartRecord { get; set; }
}
public class LocationPart:ContentPart<LocationPartRecord> {
LocationsPartRecord LocationsPartRecord {
get { return Record.LocationsPartRecord; }
set { Record.LocationsPartRecord = value; }
}
}
LocationsPartRecord and LocationsPart (container)
public class LocationsPartRecord:ContentPartRecord {
public LocationsPartRecord()
{
Locations = new List<LocationPartRecord>();
}
[CascadeAllDeleteOrphan]
public virtual IList<LocationPartRecord> Locations { get; set; }
}
public class LocationsPart:ContentPart<LocationsPartRecord> {
public LocationsPart() {
Locations = new List<LocationPart>();
}
public readonly LazyField<IList<LocationPart>> _locations = new LazyField<IList<LocationPart>>();
public IList<LocationPart> Locations {
get { return _locations.Value; }
set { _locations.Value = value; }
}
}
From here I am stuck. I would like to see when Creating a new supplier, I get a screen containing all the content item fields for supplier and a list of locations, with the ability to create, delete or update a location.
I don't need the code to be spelled out, but a direction would suffice. Which drivers, controllers and views should I create. This is only for admin console. For frontend the locations need to be displayed and not edited.
I don't think there will be any way to get the functionality you're after without custom coding. As you have suggested, the comments module could be a good example to copy. The Controllers in the comments module are only to manage all of the comments in their own admin pages, separate to the content items they belong to. The edit / display of the comments is still provided through the drivers and handlers.
Using the Comments module analogy:
CommentsPart = AddressesPart - This would be added to your Supplier content type
CommentPart = AddressPart - This would be added to your Address content type
You could strip out a lot of the extra functionality that is included for managing comments and just copy the drivers, handlers, views and models for these two parts.
I have seen some gallery modules that may allow you to build these relationships through the admin interface, however I haven't used it myself:
http://gallery.orchardproject.net/List/Modules/Orchard.Module.Downplay.Mechanics
Address shouldn't be a part, it should be a field. This way, you can have more than one, and each can be named.
Don't know if this would be helpful (and the site appears to be down - but Google has a cached version if you are patient for it to load), but there is a good blog about exactly your situation. It's Skywalkers excellent Web Shop series. I believe Part 8 contains the code related to multiple addresses (uses Address and Addresses). This seems to involve your problem, and the code may be what you need.
In case you have trouble getting to the site, there is also a CodePlex repository for the code. Additionally, Bertrand's Nwazet Commerce module might have similar code.
Related
I've been doing some work on an app using ReactiveUI.
My issue is with UITableViews and the reusing of cells. I tried using ReactiveTableViewSource, but that didn't seem to give me the level of customisation I wanted (custom header and footer views).
So I used UITableViewSource and UITableViewCell and implemented IViewFor.
I then did the binding in the individual cells.
That works fine, but I'm worried about "re-binding" to the new ViewModel every time a cell is reused. I believe I've worked around it, but I'm sure there's a better way to do it.
Code I'm using (only the relevant bit):
public partial class FlavourTableViewCell : UITableViewCell, IViewFor<MenuItemViewModel.FlavourSetItemViewModel>
{
public MenuItemViewModel.FlavourSetItemViewModel ViewModel { get; set; }
object IViewFor.ViewModel { get { return ViewModel; } set { ViewModel = (MenuItemViewModel.FlavourSetItemViewModel)value; } }
List<IDisposable> bindings = new List<IDisposable> ();
// Called when new data should be shown in cell
internal void Update (MenuItemViewModel.FlavourSetItemViewModel data, MenuItemViewModel.FlavourSelectionEnum type)
{
ViewModel = data;
// Clear old bindings
if (bindings.Any ()) {
bindings.ForEach (x => x.Dispose ());
bindings.Clear ();
}
bindings.Add (this.Bind (ViewModel, x => x.IsSelected, view => view.SelectionButton.Selected));
}
}
Additional info:
FlavourSetItemViewModel contains a list of FlavourSetItemViewModel. I'll try to explain below:
FlavourSetItemViewModel - Has section name
FlavourSetItemViewModel - Has item name
FlavourSetItemViewModel - Has item name
FlavourSetItemViewModel - Has item name
FlavourSetItemViewModel - Has item name
FlavourSetItemViewModel - Has section name
FlavourSetItemViewModel - Has item name
What you're doing doesn't look wrong to me. What you could improve is using a CompositeDisposable instead of a List of IDisposable.
But you could also try using ReactiveTableViewSource by using your own implementation and then overriding GetViewForHeader to supply your own header views.
You can then bind your data to your custom table view source:
this.WhenAnyValue (view => view.ViewModel.Section1, view => view.ViewModel.Section2, (section1, section2) => new TableSectionInformation<TModel> [] {
new TableSectionInformation<TModel, TReactiveTableViewCell> (section1, "section1cell", 44),
new TableSectionInformation<TModel, TReactiveTableViewCell> (section2, "section2cell", 44)
})
.BindTo (tableViewSource, x => x.Data);
If you're using a dynamic amount of sections, you could do the following:
this.WhenAnyValue (view => view.ViewModel.TableSections)
.Select (tableData => tableData.Select (section => new TableSectionInformation<TModel, TReactiveTableViewCell> (section, "cellIdentifier", 44)).ToArray ())
.BindTo (tableViewSource, x => x.Data);
Assuming TableData is an 2-dimensional list of sorts.
Take care that in my sample, changes to the list(s) are not taken into account. Only changes to the property (TableSections) itself.
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 extended the built-in User ContentType with a Content Picker Field that can be used to select multiple Video ContentItems. This gives me a video multi-picker control on the Edit page of each User.
I love how Orchard CMS makes this so elegantly simple to setup.
Now that I can associate multiple Videos with a User, I'd like to create a Query that will display just the Videos that the currently logged in User has been granted access.
I was hoping to be able to setup a Query using the Projector module, in what I thought was the obvious way (see below), but this returns no results.
This is how I configured the second filter:
Clicked on the + Add a new Filter link on the Edit Query screen
Chose Videos:Ids from the User Content Fields section, like this:
Configured the new filter like this:
What am I doing wrong, or what is the simplest way of diagnosing this issue?
This is how the Content Picker field is defined:
I have spotted my error - it was due to me not having a proper understand of how the filters worked. The Videos:Ids filter in the User Content Fields section does not give access to the current user's list of videos, as I assumed. Instead, it is offering the field to be used in the filter, which would be useful if I were to write a query to produce a list of Users that had access to a specific Video.
It was wishful thinking that it worked the way I wanted, but it's obvious in retrospect how it actually works.
Update: in the hope it's useful for others, here's the custom filter I developed:
public interface IFilterProvider : IEventHandler
{
void Describe(dynamic describe);
}
public class CurrentUserVideosFilter : IFilterProvider
{
private readonly IWorkContextAccessor _workContextAccessor;
public CurrentUserVideosFilter(IWorkContextAccessor workContextAccessor)
{
_workContextAccessor = workContextAccessor;
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public void Describe(dynamic describe)
{
describe.For("My Filter Category", T("My Filter Category"), T("My Filter Category"))
.Element("Current User's Videos", T("Current User's Videos"), T("Current User's Videos"),
(Action<dynamic>)ApplyFilter,
(Func<dynamic, LocalizedString>)DisplayFilter,
null
);
}
public void ApplyFilter(dynamic context)
{
var query = (IHqlQuery)context.Query;
context.Query = query.ForType("Video")
.Where(x => x.ContentPartRecord<IdentityPartRecord>(), x => x.InG("Id", GetVideoIdsForCurrentUser()));
}
private IList<int> GetVideoIdsForCurrentUser()
{
var currentUser = _workContextAccessor.GetContext().CurrentUser;
if (currentUser == null) return new int[0];
dynamic item = currentUser.ContentItem;
var videoContentItems = (IEnumerable<ContentItem>)item.User.Videos.ContentItems;
return videoContentItems.Select(i => i.Id).ToList();
}
public LocalizedString DisplayFilter(dynamic context)
{
return T("Videos that have been assigned to the currently logged in user");
}
}
I created this class in a new Orchard module, which contains all my customisations for the site I'm building. Once I installed the module, the filter was immediately available. I assume Orchard uses reflection to seek out all types that implement the IFilterProvider interface.
This is how the filter appears on the Add a Filter screen:
Clicking on the filter shows this screen:
Once the filter has been saved, the query works exactly how I'd like - it shows all videos that have been assigned to the currently logged in user.
Using Grails (or hibernate), I was wanting to know if there is a specific design pattern or method we should be using when implementing a SEARCH of our domain.
For example, on my website, I want to be able to filter(or search) by multiple properties in the domain.
EG: For I have a page which displays a list of HOTELS. When I submit a search form, or if a user clicks "filter by name='blah'", when I enter the controller I get the following:
Domain
String name
String location
Controller
if(params.name && params.reference) {
// Find name/reference
} else if(params.name) {
// Find name
} else if(params.reference) {
// Find reference
} else {
// Find all
}
As you can understand, if there are more properties in the domain to search/filter, the longer the controller gets.
Any help. Please note, I do not want to use the 'searchable' plugin, as this is too complex for my needs.
I would embed these in a named query in the Domain class itself. For example:
Class Hotel {
String name
String city
String country
boolean isNice
static namedQueries = {
customSearch { p ->
if (p?.name) eq('name', p.name)
if (p?.city) eq('name', p.city)
if (p?.country) eq('name', p.country)
if (p?.isNice != null) eq('isNice', p.isNice)
}
}
}
Then later in a controller somewhere ...
def results = Hotel.customSearch(params)
Of course this is a very simple example, but you can expand on it using the same named query or even adding others and chaining them together.
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.