1
votes

I am currently working on an Umbraco MVC 4 project version 6.0.5. The project currently uses Vega.USiteBuilder to build the appropriate document types in the backoffice based on strongly typed classes with mapping attributes. Consequently, all my razor files inherit from UmbracoTemplatePageBase

I am coming across a road block trying to invoke a HTTP GET from a razor file. For example a search form with multiple fields to submit to a controller action method, using a SurfaceController using Html.BeginUmbracoForm.

My Html.BeginUmbracoForm looks like this

@using (Html.BeginUmbracoForm("FindTyres", "TyreSearch"))
{
 // Couple of filter fields
}

I basically have a scenario where I will like to retrieve some records from an external database outside of Umbraco (external to Umbraco Database) and return the results in a custom view model back to my Umbraco front end view. Once my controller and action method is setup to inherit from SurfaceController and thereafter compiling it and submitting the search, I get a 404 resource cannot be found where the requested url specified: /umbraco.RenderMVC.

Here is my code snippet:

public ActionResult FindTyres(string maker, string years, string models, string vehicles)
        {
            var tyreBdl = new Wheels.BDL.TyreBDL();
            List<Tyre> tyres = tyreBdl.GetAllTyres();

            tyres = tyres.Where(t => string.Equals(t.Maker, maker, StringComparison.OrdinalIgnoreCase)
                                     && string.Equals(t.Year, years, StringComparison.OrdinalIgnoreCase)
                                     && string.Equals(t.Model, models, StringComparison.OrdinalIgnoreCase)
                                     && string.Equals(t.Version, vehicles, StringComparison.OrdinalIgnoreCase)).ToList();

            var tyreSearchViewModel = new TyreSearchViewModel
            {
                Tyres = tyres
            };

            ViewBag.TyreSearchViewModel = tyreSearchViewModel;

            return CurrentUmbracoPage();
        }

I then resort to using standard MVC, Html.BeginForm (the only difference). Repeating the steps above and submitting the search, I get the following YSOD error.

Can only use UmbracoPageResult in the context of an Http POST when using a SurfaceController form

Below is a snippet of the HTML BeginForm

@using (Html.BeginForm("FindTyres", "TyreSearch"))
{
 // Couple of filter fields
}

I feel like I am fighting the Umbraco routes to get my controller to return a custom model back to the razor file. I have googled alot trying to figure out how to do a basic search to return a custom model back to my Umbraco front end view till the extent that I tried to create a custom route but that too did not work for me.

Does my controller need to inherit from a special umbraco controller class to return the custom model back? I will basically like to invoke a HTTP GET request (which is a must) so that my criteria search fields are reflected properly in the query strings of the url. For example upon hitting the search button, I must see the example url in my address browser bar

http://[domainname]/selecttyres.aspx/TyresSearch/FindTyresMake=ASIA&Years=1994&Models=ROCSTA&Vehicles=261

Therefore, I cannot use Surface Controller as that will operate in the context of a HTTP Post.

Are there good resource materials that I can read up more on umbraco controllers, routes and pipeline.

I hope this scenario makes sense to you. If you have any questions, please let me know. I will need to understand this concept to continue on from here with my project and I do have a deadline.

2

2 Answers

1
votes

There are a lot of questions about this and the best place to look for an authoritative approach is the Umbraco MVC documentation.

However, yes you will find, if you use Html.BeginUmbracoForm(...) you will be forced into a HttpPost action. With this kind of functionality (a search form), I usually build the form manually with a GET method and have it submit a querystring to a specific node URL.

<form action="@Model.Content.Url"> ... </form>

On that page I include an @Html.Action("SearchResults", "TyresSearch") which itself has a model that maps to the keys in the querystring:

[ChildAction]
public ActionResult(TyreSearchModel model){

    // Find results
    TyreSearchResultModel results = new Wheels.BDL.TyreBDL().GetAllTyres();

    // Filter results based on submitted model
    ...

    // Return results
    return results;
}

The results view just need to have a model of TyreSearchResultModel (or whatever you choose).

This approach bypasses the need for Umbraco's Controller implementation and very straightforward.

1
votes

I have managed to find my solution through route hijacking which enabled me to return a custom view model back to my view and work with HTTP GET. It worked well for me.

Digby, your solution looks plausible but I have not attempted at it. If I do have a widget sitting on my page, I will definitely attempt to use your approach.

Here are the details. I basically override the Umbraco default MVC routing by creating a controller that derived from RenderMvcController. In a nutshell, you implement route hijacking by implementing a controller that derives from RenderMvcController and renaming your controllername after your given documenttype name. Recommend the read right out of the Umbraco reference (http://our.umbraco.org/documentation/Reference/Mvc/custom-controllers) This is also a great article (http://www.ben-morris.com/using-umbraco-6-to-create-an-asp-net-mvc-4-web-applicatio)

Here is my snippet of my code:

 public class ProductTyreSelectorController : Umbraco.Web.Mvc.RenderMvcController
    {
        public override ActionResult Index(RenderModel model)
        {
            var productTyreSelectorViewModel = new ProductTyreSelectorViewModel(model);

            var maker = Request.QueryString["Make"];
            var years = Request.QueryString["Years"];
            var models = Request.QueryString["Models"];
            var autoIdStr = Request.QueryString["Vehicles"];

            var width = Request.QueryString["Widths"];
            var aspectRatio = Request.QueryString["AspectRatio"];
            var rims = Request.QueryString["Rims"];

            var tyrePlusBdl = new TPWheelBDL.TyrePlusBDL();              
            List<Tyre> tyres = tyrePlusBdl.GetAllTyres();

            if (Request.QueryString.Count == 0)
            {
                return CurrentTemplate(productTyreSelectorViewModel);
            }

            if (!string.IsNullOrEmpty(maker) && !string.IsNullOrEmpty(years) && !string.IsNullOrEmpty(models) &&
                !string.IsNullOrEmpty(autoIdStr))
            {
                int autoId;
                int.TryParse(autoIdStr, out autoId);

                tyres = tyres.Where(t => string.Equals(t.Maker, maker, StringComparison.OrdinalIgnoreCase) &&
                                         string.Equals(t.Year, years, StringComparison.OrdinalIgnoreCase) &&
                                         string.Equals(t.Model, models, StringComparison.OrdinalIgnoreCase) &&
                                         t.AutoID == autoId)
                             .ToList();

                productTyreSelectorViewModel.Tyres = tyres;
            } 
            else if (!string.IsNullOrEmpty(width) && !string.IsNullOrEmpty(aspectRatio) && !string.IsNullOrEmpty(rims))
            {
                tyres = tyres.Where(t => string.Equals(t.Aspect, aspectRatio, StringComparison.OrdinalIgnoreCase) &&
                                         string.Equals(t.Rim, rims, StringComparison.OrdinalIgnoreCase)).ToList();

                productTyreSelectorViewModel.Tyres = tyres;
            }




            var template = ControllerContext.RouteData.Values["action"].ToString();

            //return an empty content result if the template doesn't physically 
            //exist on the file system
            if (!EnsurePhsyicalViewExists(template))
            {
                return Content("Could not find physical view template.");
            }

            return CurrentTemplate(productTyreSelectorViewModel);
        }
    }

Note my ProductTyreSelectorViewModel must inherit from RenderModel for this to work and my document type is called ProductTyreSelector. This way when my model is returned with the action result CurrentTemplate, the Umbraco context of the page is retained and my page is rendered appropriately again. This way, all my query strings will show all my search/filter fields which is what I want.

Here is my snippet of the ProductTyreSelectorViewModel class:

 public class ProductTyreSelectorViewModel : RenderModel
    {
        public ProductTyreSelectorViewModel(RenderModel model)
            : base(model.Content, model.CurrentCulture)
        {
            Tyres = new List<Tyre>();
        }

        public ProductTyreSelectorViewModel(IPublishedContent content, CultureInfo culture)
            : base(content, culture)
        {
        }

        public ProductTyreSelectorViewModel(IPublishedContent content)
            : base(content)
        {
        }

        public IList<Tyre> Tyres { get; set; }
    }

This approach will work well perhaps with one to two HTTP GET forms on a given page. If there are multiple forms within in a page, then a good solution will may be to use ChildAction approach. Something I will experiment with further.

Hope this helps!