Creating a shape from an MVC controller

Topics: Customizing Orchard, Writing modules
Oct 5, 2011 at 5:28 PM

Hi

I'd like to create a shape from a controller. I'm trying to use IShapeFactory, but I'm struggling with the syntax of the NamedEnumerable.

Anyone know if 1) this is a good solution and 2) how to create a shape.

 

Thanks

Coordinator
Oct 5, 2011 at 5:52 PM

Creating a shape from a controller is extremely easy and there are many examples throughout the code. Most of the time you'll want to create a shape from a content item. For example, the blog post controller does this:

dynamic model = _services.ContentManager.BuildDisplay(postPart);
return new ShapeResult(this, model);

But you can build an arbitrary shape this way:

var myShape = Shape.MyShape(Foo: 42, Bar: "yay!");
Oct 5, 2011 at 9:13 PM

It's the second version I'm interested in. I'm not displaying a part, but data from an external system.

So what is 'Shape' in 

var myShape = Shape.MyShape(Foo: 42, Bar: "yay!");
And how do I access it from a controller?
Coordinator
Oct 5, 2011 at 9:57 PM

In the very same file, BlogPostController, you can find the answer ;)

        public BlogPostController(
            IOrchardServices services, 
            IBlogService blogService, 
            IBlogPostService blogPostService,
            IFeedManager feedManager,
            IShapeFactory shapeFactory) {
            _services = services;
            _blogService = blogService;
            _blogPostService = blogPostService;
            _feedManager = feedManager;
            T = NullLocalizer.Instance;
            Shape = shapeFactory;
        }

        dynamic Shape { get; set; }

Oct 6, 2011 at 9:25 AM

Seek and ye shall find :) I used to be good at that! Thanks again.

Oct 13, 2011 at 3:46 AM
Edited Oct 13, 2011 at 4:24 AM

I don't want to hijack your thread, but can we investigate the dynamic model method a little further as well?

In my controller, I have the following

 

        [HttpGet]
        public ActionResult List()
        {
            dynamic model = _orchardServices.ContentManager.BuildDisplay(unitPart);
            return new ShapeResult(this, model);
        }

 

Is it correct to define unitPart as such?

 

UnitPart unitPart = _unitService.Get();

 

where it calls the following in a service file (I just hard coded an ID of 1 for now...and there is a record in the table with an ID of 1):

 

        public UnitPart Get()
        {
            return _contentManager.Query<UnitPart, UnitPartRecord>().Where(u => u.Id == 1).List().FirstOrDefault();
        }

 

Once that part is done, is the Display method in UnitPartDriver correct if I define it as such?

 

        protected override DriverResult Display(UnitPart part, string displayType, dynamic shapeHelper)
        {
            return ContentShape("Parts_Unit",
                () => shapeHelper.DisplayTemplate(
                    TemplateName: "Parts/Unit",
                    Model: part,
                    Prefix: Prefix));
        }

 

Finally, I have a file in Views/DisplayTemplates/Parts named Unit.cshtml defined with some simple text in it to see if I can get to it.

I definitely have something wrong somewhere because I get the following error when I attempt to go to /Unit which is defined in my Routes.cs file pointing to the Controller and Action List

 

[NullReferenceException: Object reference not set to an instance of an object.]
   Orchard.ContentManagement.DefaultContentDisplay.BuildDisplay(IContent content, String displayType, String groupId) in D:\Source\Scoutopia\src\Orchard\ContentManagement\DefaultContentDisplay.cs:44
   Orchard.ContentManagement.DefaultContentManager.BuildDisplay(IContent content, String displayType, String groupId) in D:\Source\Scoutopia\src\Orchard\ContentManagement\DefaultContentManager.cs:478
   Raptor.Scoutopia.Controllers.UnitController.List() +127

When I do a trace, content is null and displayType and groupId are both empty strings, so nothing is getting passed to this method. If I put a breakpoint in the controller, it looks like my call to the service is returning null. This would make me believe that I'm not querying the ContentManager correctly. Any hints on how to format that correctly?

Oct 13, 2011 at 12:21 PM

The content item with Id == 1 is usually the Site object. If you've added UnitPart to the Site content type then that query would work as expected; otherwise, it'll return null because the content item you're querying for doesn't exist.

Oct 13, 2011 at 8:56 PM
Edited Oct 13, 2011 at 9:02 PM

ahhh...I think that's where my first problem is. Since the record is actually in the UnitPartRecord table, not a content item table, I'm thinking I need to retrieve it from that table instead.

If I'm using IRepository to retrieve it from the UnitPartRecord table, how do I then convert it to use it with the shape? Querying with IRepository returns a UnitPartRecord object and it would appear the driver is looking for a UnitPart object.

Thanks!

 

EDIT

That could be a dumb question. I suppose I could just do this:

UnitPartRecord unit = _unitPartRepository.Get(1);

return new UnitPart
{
    UnitNumber = unit.UnitNumber,
    UnitName = unit.UnitName
};

Oct 13, 2011 at 9:23 PM
Edited Oct 13, 2011 at 9:23 PM

I still come up with a null result, but it's throwing that error in the UnitPart model file on the set { Record.UnitNumber = value; } line. I even tried hard coding values into a new UnitPart object and passing that...same thing.

Coordinator
Oct 13, 2011 at 9:35 PM

In the code you've provided so far, I don't see anything named "content" and yet you say it is null. Hard to diagnose given we are missing the crucial piece of information here. But right, content manager is never going to find orphan parts that are not attached to an item. This should never be done as a matter of facts. It's fine to use records without parts and items though. Once you get something back from your repository, just create a new shape and set properties on it. Shape.Whatever(Something: myRecord.Something).

Developer
Oct 13, 2011 at 9:38 PM

Regarding your last question - no, you cannot do that. Never try to create a ContentPart<TRecord>-deriving object directly, using "new".

Content part has a corresponding, underlying Record - it's why null in your example and the NullReferenceException is being thrown. When you try to set the UnitNumber property, it tries to pass the value to the underlying Record, which is null at this moment.

Oct 13, 2011 at 9:48 PM

Okay...so it sounds like my call to build the shape in the Controller using

dynamic model = _orchardServices.ContentManager.BuildDisplay(unitPart);
return new ShapeResult(this, model);

is incorrect since it's not a content item. I'll switch over to using records without having them be parts and items since it's not necessary for this project.

Do you know of an example I can follow for building shapes out of records? Otherwise I'm pretty sure I'm going to have to ask what "Something" should be referring to. I believe "Whatever" is just a dynamic name I want to use, correct? 

When I make this call in the Controller, will it then look to the Driver to build the shape, or am I telling it directly in the Controller what parts to use to build the shape? As you can see, this process is still a bit confusing to me.

Oct 13, 2011 at 11:33 PM

ahhh...I see what you mean now by  Something. It would be a call like:

var unitShape = Shape.UnitShape(UnitNumber: unitRecord.UnitNumber, UnitName: unitRecord.UnitName);

if I'm understanding correctly, that will create a dynamic shape. I've been reading the document here:

http://orchardproject.net/docs/Accessing-and-rendering-shapes.ashx#Rendering_Shapes_Using_Templates_8

to get a better understanding of shapes and it's starting to come together. What I'd like to do is use shape templates though...is that possible from a Controller using a Record instead of Part and Item? Can you point me to an example of the syntax for doing that?

Thanks again guys!

Coordinator
Oct 13, 2011 at 11:45 PM

When you add a shape into the shape tree, you know that it will be resolved into a shape by Orchard, base don its name, alternates, etc. So you don't need anything more than a template with the right name in your views folder in your theme. You can create a shape from anywhere using any data. It doesn't care.

Oct 14, 2011 at 8:59 PM

Okay, that has to be the coolest thing I've ever seen. I passed in static values, assigned values from a model, an model object, a viewmodel. It takes EVERYTHING.

So...let me make sure I have this correct before I go hog wild and start using it for everything.

I created a Route for /Unit that points to UnitController and an action called List

The List ActionResult looks simply like this right now

[HttpGet]
public ActionResult List()
{
    var unit = _unitService.Get(1);
    var model = Shape.Unit(unit);

    return new ShapeResult(this, model);
}

the Get method in _unitService just retrieves a UnitRecord with an Id of 1.

I placed a file called Unit.cshtml in the View folder which looks simply like this:

You made it to the Unit Template<br />
@Model.UnitNumber<br />
@Model.UnitName

and it rendered everything perfectly. So here are my brief questions based on this:

1. When I declare Shape.Unit - Unit is the name of the template it's looking for, correct?

2. If I wanted to build a display that had say a Unit Shape and a Scout Shape rendered on the same page, how would I add multiple shapes in the controller so that it pulls both templates and renders them on the same page?

3. Can I have the controller return a regular View and also render shapes on that page?

4. For Shapes that are for entering data, do I place the templates in an EditorTemplates folder?

Coordinator
Oct 14, 2011 at 9:13 PM

1.

Yes. And you can also add alternates to this shape, so that more specific or context based templates are taken instead.

2.

var model = Shape.MyCustomShape(Scout: Shape.Scout(), Unit: Shape.Unit());
return new ShapeResult(model);

3.

var model = Shape.MyCustomShape(Scout: Shape.Scout(), Unit: Shape.Unit());
return View(model);

and inside the template:

@Display(Model.Scout);
@Display(Model.Unit);

4.

No, the EditorTemplates folder is just a convention for shapes that are created by Drivers. Because right now (not the case in dev branch), Drivers editors don't suport Shapes out of the box.

Oct 14, 2011 at 9:17 PM

Perfect...thank you so much!

Oct 14, 2011 at 9:43 PM

A question regarding Placement.info...

Can I use Placement.info to determine where the template will get rendered or do I have to declare that differently when I'm not using content items?

I tried this

<Placement>
  <Place Unit="TripelThird:1" />
</Placement>
but it didn't move the template to that zone. If the template is named Unit.cshtml, do I have the name in Placement.info correct? Thanks

Coordinator
Oct 14, 2011 at 10:40 PM

The placement is only used for the shapes rendered by a driver, in a content.

What you need to do, is when you add a shape, to add it to the Layout:

Layout.TripleThrid.Add(Shape.Unit(), "1");

or

Layout .TripleThird.Add(Model.Unit, "1")

Oct 14, 2011 at 11:31 PM

Gotcha...and that would have to go in a View?

In the example above where I'm simply rendering the template from the Controller, can I specify the zone there or is the only way to specify placement using this method in the View?

Thanks again for the help

Coordinator
Oct 14, 2011 at 11:46 PM

You might want to try Layout["TripleThird"].Add(), and if this works, then you can pass the zone name from the controller.

Oct 18, 2011 at 2:37 AM

Actually, it ended up being:

@{Layout.TripelThird.Add(Model.Unit, "1");}
but thanks...you got me in the right direction =) this is perfect. You guys are the best!

Nov 21, 2011 at 8:23 PM
Edited Nov 21, 2011 at 8:39 PM

Ran into another question today and hope someone can help.

In my controller, I'm passing 2 shapes to a View

 

var shapeModel = Shape.BenefitAssignment(Available: Shape._availableShape(_benefitService.Get()), Assigned: Shape._assignedShape(_benefitassignmentService.Get(ReportId)));
return View(shapeModel)

 

In the View, I have the following

 

@{Layout.TripelFirst.Add(Model.Available, "1");}
@{Layout.TripelThird.Add(Model.Assigned, "1");}

 

and of course I have 2 shape templates named _availableShape.cshtml and _assignedShape.cshtml

The problem I'm having, is I don't believe I'm passing the Model data for each of the shapes correctly. The model data for each is an IEnumerable of a model and simply trying to iterate over that IEnumerable and display one of the properties comes back with a NULL exception, which is why I believe I'm not passing the model data from the View to the shape templates correctly, or I'm not referencing the model data in the shape template correctly.

Since it's a dynamic shape, I was under the impression I had to cast the shape proxy, so I did this in the shape template

 

@foreach (var benefit in Model as IEnumerable<BenefitRecord>)

 

but that doesn't work.

Any ideas on what I might be doing wrong? Thanks.

Nov 21, 2011 at 8:37 PM

Instead of passing an IEnumerable<BenefitRecord> to the _availableShape, I passed just a single BenefitRecord object and was able to access the properties fine in the shape template. Is there a different way to pass an IEnumerable through that I'm doing wrong?

Developer
Nov 21, 2011 at 9:51 PM

Model is always a dynamic proxy, so remember to use named arguments when passing info to shapes. So instead of:

var shapeModel = Shape.BenefitAssignment(Available: Shape._availableShape(_benefitService.Get()), Assigned: Shape._assignedShape(_benefitassignmentService.Get(ReportId)));
return View(shapeModel)

Write:

var shapeModel = Shape.BenefitAssignment(Available: Shape._availableShape(Items: _benefitService.Get()), Assigned: Shape._assignedShape(Items: _benefitassignmentService.Get(ReportId)));
return View(shapeModel)

Notice the Items named property.

Then you can access your IEnumerable<Something> via Model.Items inside _availableShape.cshtml and _assignedShape.cshtml, as usual.

Nov 21, 2011 at 9:53 PM
Edited Nov 21, 2011 at 9:53 PM

oops, we posted at the same time =)

Nov 21, 2011 at 9:56 PM

Thanks Peter!

In my shape template, I changed the loop to

@foreach (var benefit in Model.Items)
and it worked great.

Nov 21, 2011 at 10:02 PM

Hit another snag...where I'm using the data in the shape template, I need to be in the original format of IEnumerable<BenefitRecord>, however, it's coming through as a List<object>.

I tried casting it using (IEnumerable<BenefitRcord>)Model.Items and Model.Items as IEnumerable<BenefitRecord>, but neither seemed to work.

Is there a way to make it come through as the original object type? Thanks again!

Developer
Nov 21, 2011 at 10:21 PM

Ahh right, if you pass the collection as dynamic parameter, the collection items are treated as being of object type.

IEnumerable<T> is a covariant type - you cannot cast IEnumerable<object> to IEnumerable<BenefitRecord> (but IEnumerable<BenefitRecord> to IEnumerable<object> - yes).

Try using Linq Cast<T>() method to explicitly cast each item to the proper class instead:

@{
   var items = ((IEnumerable<object>)Model.Items).Cast<BenefitRecord>();
}

foreach(var item in items)
{
   ...
}
Nov 21, 2011 at 10:53 PM

That worked great! Thanks again Peter!