Custom module, trying to render view outside of @Zone(Model.Content)

Topics: General, Writing modules
Nov 14, 2013 at 9:30 PM
I have a custom module that is [Themed] and has been (with Routes and Controllers) working well. The views are used to capture user information and just saved as plain records in Orchard.

The default it seems in Orchard is to send that to @Zone(Model.Content) in the Theme's Layout.cshtml file.

I want to continue using the module but send it to another zone and not have it display in the Content zone anymore at all. Reason being I want to create the page in Orchard's Admin (text + images), and then just have the form display in an AfterContent zone.

Is this possible without creating a content type/part? If so, can you give me some examples (or point to one)?

Thanks.
Nov 15, 2013 at 9:21 AM
Yes that's possible:

Use placement for that like:

<Place shapeName="/AfterContent:1" />

Use the / prefix to place your shape in an global zone.
Nov 15, 2013 at 4:19 PM
Unfortunately I am not using a "Shape". I just have a regular MVC controller and view (am using @model.MyViewModel in the view).

I have tried using the name of the .cshtml file with placement to no avail. I have a view under ~/Views/Folder1/View1.cshtml, for example. I tried using "Folder1_View1" in placement.
Nov 15, 2013 at 5:06 PM
Normally what you want to do would be done using widgets. You want the form to be displayed in the AfterContent and also the page in the content, right? Is there a reason why you prefer not to use widgets?
Maybe you can apply this in your controller. Instead of shapeHelper which is provided in content part drivers, you can inject IOrchardServices to controller constructor and use _orchardServices.New.ShapeName to create the dynamic shape, and put the ShapeName.cshtml in the Views folder root, I guess. Rendering the page in the content zone is a different story. You would need to call a pageShape = _orchardServices.ContentManager.BuildDisplay(pageContentItem) for the page content item and return a ShapeResult(this, pageShape) in the action method. I haven't tried this.




Nov 15, 2013 at 5:46 PM
Edited Nov 15, 2013 at 6:04 PM
Thanks for the pointers @kassobasi. I was trying out that technique but I guess I wasn't getting the syntax correct. It worked in a sense but appeared in both the Content zone and the AfterContent zone. Sorry if it's a stupid question, but how do I stop that from happening? I thought about doing a "-" in placement, but then wouldn't that kill whatever I have in Content on that page once I load the module (and it's using my theme)?

I did this:
[Themed]
public ActionResult View1()
{
    var shape = _orchardServices.New.Folder1_View1();

    _workContextAccessor.GetContext()
    .Layout.Zones["AfterContent"]
    .Add(shape);

    return new ShapeResult(this, shape);
}

[Themed]
[HttpPost]
[ActionName("View1")]
public ActionResult View1POST(string submitButton)
{
    if ((submitButton != null) && ModelState.IsValid)
    {
        // Save to DB
        var record = new MyRecord();
        Map(myData, record);
        _repository.Create(record);

        return RedirectToAction("Submitted");
    }
    else
    {
        var shape = _orchardServices.New.Folder1_View1(MyModel: myData);

        _workContextAccessor.GetContext()
            .Layout.Zones["AfterContent"]
            .Add(shape);

        return new ShapeResult(this, shape);
    }
}
And it works in that it is sending the shape to AfterContent. However, it is still displaying in the Content zone as well (i.e., it's displayed in both zones at the same time). I suspect it's because I do ShapeResult(this, shape) (in the blog above it returns an empty DriverResult). I just don't know how to code it properly.

Thanks in advance for your help.
Nov 15, 2013 at 6:20 PM
You send the shape to AfterContent zone and return it as well, which renders it in both AfterContent and Content. You need to return null.


Nov 15, 2013 at 7:26 PM
Right on.
return new ShapeResult(this, null);
Thanks!
Nov 15, 2013 at 7:41 PM
You are welcome. You shouldn't need to return a ShapeResult, a plain return null should do the work, I guess, if Themed attribute doesn't care.

http://stackoverflow.com/questions/8561038/return-new-emptyresult-vs-return-null


Nov 17, 2013 at 6:02 PM
Edited Nov 17, 2013 at 6:05 PM
@kassobasi BTW, returning null causes the view to be empty, so doing (this, null) works well.

However, sorry to keep troubling you, but I am having problems with HttpPost, and was wondering if you might have some idea of what's wrong.

Here is my modified controller:
[Themed]
public ActionResult View1()
{
    var myViewModel= new MyViewModel();

    var shape = _orchardServices.New.Folder1_View1
        (
            MyModel: myViewModel
        );

    _workContextAccessor.GetContext()
        .Layout.Zones["AfterContent"]
        .Add(shape);

    return new ShapeResult(this, null);
}

[Themed]
[HttpPost]
[ActionName("View1")]
public ActionResult View1POST(string submitButton, MyViewModel myViewModel)
{
    if ((submitButton != null) && MyValidation()) // See below for MyValidation method
    {
        // Save to DB
        var record = new MyRecord();
        Map(myViewModel, record); // See below for Map method
        _repository.Create(record);

        return RedirectToAction("Submitted");
    }
    else
    {
        var shape = _orchardServices.New.Folder1_View1
            (
                MyModel: myViewModel
            );

        _workContextAccessor.GetContext()
            .Layout.Zones["AfterContent"]
            .Add(shape);

        return new ShapeResult(this, null);
    }
}

// Mapping
private void Map(MyViewModel myViewModel, MyRecord record)
{
    record.FirstName = myViewModel.FirstName;
    record.LastName = myViewModel.LastName;
}

// Validation
private bool MyValidation()
{
    bool validate = true;
    var myModel = new MyViewModel();

    if (String.IsNullOrEmpty(myModel.FirstName))
    {
        AddModelError("FirstName", T("Please enter your first name."));
        validate = false;
    }

        if (String.IsNullOrEmpty(myModel.LastName))
    {
        AddModelError("LastName", T("Please enter your last name."));
        validate = false;
    }

        if (!validate)
    {
        _orchardServices.Notifier.Error(T("There are some errors. Please correct them and submit this form again."));
        return false;
    }
       
        return ModelState.IsValid;
}
Now, I am trying to validate inside the controller for now (plan to separate that out later). Doing my own validation so that it's more detailed than stock localization/validation in Orchard.

In my View1.cshtml (sample portion):
@{
    var model= (MyViewModel)Model.MyModel;
}
@using (Html.BeginFormAntiForgeryPost(Url.Action("View1", "Folder1", new { area = "MyModule" }), FormMethod.Post))
{
@Html.LabelFor(m => model.FirstName, T("First Name:"))
<span class="validation">@Html.ValidationMessageFor(m => model.FirstName)</span>
@Html.TextBoxFor(m => model.FirstName, new { placeholder = "Your First Name" })

@Html.LabelFor(m => model.LastName, T("Last Name:"))
<span class="validation">@Html.ValidationMessageFor(m => model.LastName)</span>
@Html.TextBoxFor(m => model.LastName, new { placeholder = "Your Last Name" })
} 
Now, I've tried looking at a lot of other modules to get this right. I've done a lot of Googling. But I cannot get this to work right.

What's happening is everything is rendered fine. But it's not working properly. Here's what I mean:
  1. If I hit submit without entering any information in either First Name or Last Name, it returns the page with the validation errors ("Please enter your first name.", etc.). Fields are returned empty.
  2. If I fill in the field for First Name but not Last Name, I get the validation error for BOTH and the view is returned with both fields empty. My expectation is a validation error for the Last Name and that upon return the First Name would be filled in with whatever I input.
Sorry if I am making a noob mistake. But I just cannot figure this out. I want to utilize Orchard's way of doing things rather than just render a page that is going to utilize the Content zone. The ultimate purpose being that I want to display several "widgets" on the page in separate zones.

Thanks again for any help/advice/code samples.
Nov 17, 2013 at 6:53 PM
Good to know about null, thanks.

Does the controller work when both text boxes are filled? Where is the submit button in the view? It is supposed to be in the @using block of BeginForm, I think. Maybe it's mapped to empty string instead of null and myViewModel is empty as well.

Try adding a submit input with name submitButton, and renaming the model variable in the view to myViewModel to match the post action.

You might also want to attach a debugger.

Good luck.


Nov 20, 2013 at 7:31 PM
Just as an update for posterity, I was not passing the view model with the custom validation inside the HttpPost portion of the controller, so that it appears like this:
if ((submitButton != null) && MyValidation(myViewModel))
And also changing the validation method to:
private bool MyValidation(MyViewModel myViewModel)