Don't miss out Virtual Happy Hour this Friday (April 26).

Try our conversational search powered by Generative AI!

EPiServer MVC - How do I use forms and controllers correctly?

Vote:
 

Hi guys, I've asked several questions about a currentPage problems here on EPiserver World and tried to find something about it in the documentation, but I found nothing and I cannot seem to grasp how to use EPi/MVC correctly.

I know in MVC you make a form post like this:


@using (Html.BeginForm("Action", "Controller", FormMethod.Post))
{                                   
    
}

And is recieved in a controller:

public ActionResult Index()
{
    return View("View");
}


But in EPiServer MVC it needs to recieve a page:

public ActionResult Index(PageType currentPage)
{
    return View(currentPage);
}


As long as the post is done on the PageType of that controller it works fine, but how should I do if I want to post to another controller, from another PageType?
Ex: StartPage post to StandardPage controller, which searches for a StandardPage to recieve, but doesn't recieve one - leading to a null valued currentPage.

I've tried using the ServiceLocator and PageContext to get the requested page, but all it gives me is the page I came from, which in this example is the StartPage.
I'm not using multiple languages and I already know you can add that to the form.

Been trying everything I found here on the forums, on Stack Overflow and other sources, but I find very little about EPi/Mvc so I'm hoping you guys have the knowledge I'm looking for.

I'm happy to ellaborate my problems if you want to know more about what I want to do.


Regards
Ludvig Flemmich

#113672
Nov 26, 2014 12:01
Vote:
 

Hi!

One solution to post to another page in EPiServer would be to post to the pages friendly url and append the action name to the url.

<form action="@Url.AddSegment(ContentReferenceOfStandardPageToPostTo, "MyPostActionName")" method="post">
</form>

Url.AddSegment is a custom UrlHelper extension method:

public static string AddSegment(this UrlHelper helper, ContentReference contentLink, string segment)
{
    var builder = new UrlBuilder(helper.ContentUrl(contentLink));
    builder.Path = VirtualPathUtility.AppendTrailingSlash(builder.Path) + segment;
    return builder.ToString();
}

                        
#113674
Edited, Nov 26, 2014 12:29
Vote:
 

Hello! :)

Ok, and how can I get the desired ContentReference (other than StartPage) in a good and fairly smooth way for this method?

#113676
Nov 26, 2014 13:05
Vote:
 

Well, if it doesn't matter which one of the standard pages in your site you post to you can get a page ID from the edit tree by hovering any standard page node. Then just use "new ContentReference(ID)" in the Url.AddSegment method. However, that's a pretty ugly hardcoded solution. A better option would be to add a property to the start page of type ContentReference and then browse the standard page you want to post to in that property. In your view you can then get the property value from the start page.

Why is the action method located in the StandardPage controller by the way? Could it be located in a base controller used by all page types or in the StartPage controller? If it would be located in a base controller you could always post to current page. <form action="@Url.AddSegment(CurrentPage.ContentLink, "MyPostAction")" method="post"></form>

#113680
Nov 26, 2014 13:41
Vote:
 

Because that's how I know how to do it unfortunately, but this solution about a "global" controller for all PageTypes sounds like a good idea!
Would you mind explaining more about how I can achieve this with your method? :)

#113684
Nov 26, 2014 14:05
Vote:
 

Could you describe the scenario a bit? What is it that you are trying to accomplish? Do you have a form that's displayed on all pages?

#113729
Edited, Nov 27, 2014 7:57
Vote:
 

This example allows you to have a form on all pages:

Edit model example:

public class MyEditModel
{
    [Required]
    [Display(Name = "Name*")]
    public string Name { get; set; }
}

_MyForm partial:

@model MyEditModel

@{
    var pageRouteHelper = ServiceLocator.Current.GetInstance<PageRouteHelper>();
}

<form action="@Url.AddSegment(pageRouteHelper.PageLink, "MyPostAction")" method="POST">
    @Html.AntiForgeryToken()
    @Html.ValidationSummary()

    @Html.TextBoxFor(m => m.Name)
    <button type="submit">Submit</button>
</form>

Render _MyForm in your layout or in a page view:

@{ Html.RenderPartial("_MyForm", new MyEditModel()); }

Base page controller with post action:

Make sure all your page controllers derives from this class to support posting.

public abstract class BasePageController<T> : PageController<T> where T : PageData
{
    protected virtual T CurrentPage
    {
        get
        {
            return PageContext.Page as T;
        }
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult MyPostAction(MyEditModel editModel)
    {
        if (ModelState.IsValid)
        {
            // Do something with the edit model data here.

            return RedirectToAction("Index");
        }

        // If ModelState is not valid, return custom ActionResult that invokes the "Index" action on current controller.
        return new FormPostedResult();
    } 
}

Custom ActionResult that invokes the Index action on the current controller:

public class FormPostedResult : ActionResult
{
    public override void ExecuteResult(ControllerContext context)
    {
        ((Controller) context.Controller).ActionInvoker.InvokeAction(context, "Index");
    }
}
#113731
Nov 27, 2014 9:04
Vote:
 

Ludvig, I think what you want to do is this:

@Html.BeginForm("Index", "Start", FormMethod.Post, new { node = ContentReference.StartPage })
{                                   
    <button type="submit"></button>
}

The above code will post the form to StartController.Index, and this can be used on any page. The only "trick" is that you need to specify a content reference in the node parameter.

#113771
Nov 27, 2014 14:38
Vote:
 

The problem with posting from one page to another is you'll be redirected and that might be confusing for the end user in some scenarios. You don't expect to end up on a totally different page when you submit a form. As a developer, it would be messy to handle model state and redirects back to the posting page to show validation errors, success messages etc if required.

If everything is handled with Ajax it's a different question though. And if no form post is required ( example, when you just want to execute the result of another action than Index and no post variables needs to be submitted), just a standard link with friendly url to a page with the action name applied to the end of the url should be fine.

#113779
Nov 27, 2014 16:07
Vote:
 

Not sure why all the posts have changed order of appearance, but I'll try to explain further.

The scenario is like this:

I have a contentarea on my startpage which has several different blocks for different purposes. What I'm building is an intranet and these blocks contain different information about the logged in user, what office group he/she is in, upcoming events for that group, etc.
All of these blocks shows information in a very basic fashion and also has a Page counterpart which displays all information, what I'm trying to do is to post from the block and land on the Page counterpart.

Example:
I have a profileblock that displays your image, office status (ex: At the office, Absent, At client), a field that displays preferred contact information (cellphone number, email or w/e you want) and a personal message which is optional.
If I don't have a picture a link is displayed saying "You don't have a profile picture, click here to upload one." and when you click it you make a post from the ProfileBlock and should land on the ProfilePage with the EditProfile action to be able to add an image or just edit w/e information you want.
THIS is where the problem is, because when I post from the blockview and recieve the post in the ProfilePageController the currentPage parameter gets null.

I've tried several solutions, but all of them seem disgraceful or just wrong, so I'm asking if there's any standard way of doing it dynamically as I do not want to hardcode the page ID's etc.
I have used a global pageReference which I set in the StartPage before, but It feels wrong even though it works, but maybe I'm just picky?

Also I'm fairly new to EPiServer MVC, so I don't know any good ways to get ContentReferences of the desired type, I made a method that searches through the StartPages children with a string parameter for the PageTypeName and then cast it to the PageType I want, but somehow it doesn't work the way I want it to either.

Any good tips for how I can get a ContentReference of specified type except for the global page/contentreference set in the CMS?


Thanks in advance
Ludvig Flemmich

#113803
Nov 28, 2014 10:22
Vote:
 

Hello again.

Adding global PageReference/ContentReference properties to the start page is a fairly good practice I would say. You can hide these administrator settings in a dedicated "Site settings" tab and only display the tab for administrators. It is more efficient than trying to traverse pages in the tree to find the one of correct type.

If you still want to search child pages of a specific type in a container you can use IContentRepository for this:

var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();

IEnumerable<PageTypeToSearchFor> pages = contentRepository.GetChildren<PageTypeToSearchFor>(ContentReference.StartPage);

PageTypeToSearchFor firstOccurance = pages.FirstOrDefault();

If you need to search for pages recursively you can take a look at the FindPagesWithCriteria method in IContentRepository.

#113833
Nov 28, 2014 10:56
Vote:
 

I second what Mattias said. For the standard pages in a site, e.g. search page, profile page, checkout page - you will want to have them configured somewhere. The common way to do this is to have them as content references on the start page. If you check the Alloy MVC sample site (from Episerver), they have used this approach too.

#113834
Nov 28, 2014 11:01
Vote:
 

Johan Book, I tried this:

@Html.BeginForm("Index", "Start", FormMethod.Post, new { node = ContentReference.StartPage })
{                                  
    <button type="submit"></button>
}

currentPage was still null when I added a PageReference/ContentReference. Tried both but still null :/

#113835
Nov 28, 2014 11:02
Vote:
 

Alright then, I'll add an admin tab with standard pages as you said :)
Another question though, since it doesn't work when I add the ContentReference as a node in the form I have to do something like this to get it to work:

        public ActionResult EditProfile(ProfilePage currentPage)
        {
            if (currentPage == null)
            {
                return RedirectToAction("EditProfile", new { node = page.StartPage.ProfilePageReference });
            }
            ProfilePageViewModel model = new ProfilePageViewModel(currentPage);
            model.currentUser = ConnectionHelper.GetUserByEmail(User.Identity.Name);
            return View("EditProfile", model);
        }

page is a new, blank ProfilePage where I simply get the reference from the StartPage pointing to the profilepage.

Any way I can improve this/sort out the forms problem?

Thanks
Ludvig Flemmich

#113837
Nov 28, 2014 11:07
Vote:
 

I can't see why you need to use a form to be able to redirect from the profile block to the profile page? Just use a standard link:

<a href="@Url.ContentActionUrl(Model.ProfilePageReference, "EditProfile")"></a>

Model.ProfilePageReference is a PageReference property on your profile block view model. In your profile block controller you can set the value of this property like this:

var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();
var startPage = contentRepository.Get<StartPageType>(ContentReference.StartPage);

var blockModel = new ProfileBlockViewModel(currentBlock)
{
    ProfilePageReference = startPage.ProfilePageReference
};


Url.ContentActionUrl extension method code can be found in your previous post:
http://world.episerver.com/Modules/Forum/Pages/Thread.aspx?id=113043&epslanguage=en

If you still want to use a form for the redirect all you need to do is to pass the ContentReference of the profile page in the route data:

@using (Html.BeginForm("EditProfile", "ProfilePage", new { node = Model.ProfilePageReference }, FormMethod.Post))
{
}
#113839
Nov 28, 2014 11:28
Vote:
 

Yeah I tried adding the ContentReference as a node, but it still turned out to be null, even though it's the correct type and correct reference.
The ContentActionUrl extension gives me an error:

"'System.Web.Mvc.UrlHelper' does not contain a definition for 'ContentUrl' and no extension method 'ContentUrl' accepting a first agrument of type 'System.Web.Mvc.UrlHelper' could be found (are you missing a using directive or an assembly reference?)"

public static string ContentActionUrl(this UrlHelper urlHelper, ContentReference contentLink, string actionName)
{
    var actionUrl = new UrlBuilder(urlHelper.ContentUrl(contentLink));
    actionUrl.Path = VirtualPathUtility.AppendTrailingSlash(actionUrl.Path) + actionName;
    return actionUrl.ToString();
}


I think I added all using directives as well:

using EPiServer;
using EPiServer.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
#113842
Nov 28, 2014 11:48
Vote:
 

You probably need EPiServer.Web.Mvc.Html as well.

#113843
Nov 28, 2014 12:24
Vote:
 

Yes Erik, that's what was missing! :) Thanks!
I decided to make a tab for admins where the specify the different kinds of standardpages, logotype and other properties and utilize this with the ContentActionUrl extension, which works great!
Thanks a lot for your help and patience with me Mattias Olsson and Johan Book, I've tried marking your posts as answers but it doesn't seem to work or atleast isn't visible for me.

I'll check this post from time to time to see if your answers got accepted as correct answer or not :)

Regards
Ludvig Flemmich

#113847
Nov 28, 2014 12:31
Vote:
 

No problem Ludvig. One last thing, make sure you pass the parameters in correct order in the Html.BeginForm helper. It's easy to make a mistake and pass route values in the htmlAttributes parameter. They are of the same type. Also make sure that the ContentReference you are passing has the correct profile page ID.

By the way, I realized you don't have to use the Url.ContentActionUrl extension method. You can also use Url.Action("EditProfile", "ProfilePage", new { node = Model.ProfilePageReference }).

#113848
Nov 28, 2014 12:49
Vote:
 

Also I noticed that the form didn't work if you specified controller, so it has to be null:

@using (Html.BeginForm("Index", null, new { node = Model.ProfilePageReference }, FormMethod.Post))
{
      @Html.Hidden("Email", user.Email)
      <button type="submit" class="linkButton">@user.FirstName @user.LastName</button>
}


Yeah I tried the Url.Action, works as well! Think it's the same about the controller here though, since the node points towards which page it is it automatically enters the correct controller.
Thanks for helping me, I really appreciate it! EPiServer & MVC seems a bit easier to deal with now thanks to you! :)


Regards
Ludvig Flemmich

#113849
Nov 28, 2014 12:54
Vote:
 

Yeah, you're actually right about the controller parameter. You can specify it as long as you're on the same page though. I would just stick to Url.ContentActionUrl.

#113852
Nov 28, 2014 13:06
This topic was created over six months ago and has been resolved. If you have a similar question, please create a new topic and refer to this one.
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.