Following my foray into the Tumblr API, I have discovered that I wanted an extra bit of functionality. While the Umbraco back office does a reasonably good job on mobile, I wanted a simpler solution to be able to enter my Cinema Cento blog posts on the go. This meant that I would need a stripped-back form that could be used as part of the public site, rather than having to log into the Umbraco back office. As I didn’t want just anyone creating my blog posts, this form would need to be in a password-protected part of the site (more on this in an upcoming post).
Within my site, I have been using the LeBlender package to be able to add new grid editors to the standard Umbraco Grid Editor. Under the “Developer” section, I added a new “LeBlender” grid editor called “Cinema Cento Form”. As the form is defined within the view that I created, there is no need for any extra properties on the grid editor.
Still in the developer section, I then needed to define under which row configurations the new grid editor could be used. This is not so easy to find! For the “Grid” data type, click on the row configuration that you want to allow and then click on the width definition solid bar (labelled below). The list of allowed editors will then appear below the solid bar.
You can either allow all editors or specify them individually.
Umbraco uses the standard .NET MVC mechanism to process forms, so my first job was to create a model to hold the fields of the form.
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace rtmullinger2018.Models { public class CinemaCentoForm { public const String DATE_FORMAT = "dd/MM/yyyy"; public CinemaCentoForm() { DateString = DateTime.Today.ToString(DATE_FORMAT); } public String Title { get; set; } public float StarRating { get; set; } public String VideoUrl { get; set; } public String Description { get; set; } public String Tags { get; set; } // Extra tags (name is automatic and should not be included) public String DateString { get; set; } } }
Next, I built the view using the standard .NET notation for a form within a partial view (this will be referenced from the grid editor view later).
@model rtmullinger2018.Models.CinemaCentoForm @if (ViewData["cinema-cento-form.error"] != null && !String.IsNullOrWhiteSpace(ViewData["cinema-cento-form.error"].ToString())) { <div class="alert alert-danger"><strong>Error</strong> @ViewData["cinema-cento-form.error"]</div> } <div class="form-container"> @using (Html.BeginUmbracoForm("Submit", "CinemaCento")) { @Html.AntiForgeryToken() <fieldset> <ol> <li class="form-row text-input-row name-field"> @Html.LabelFor(l => l.Title, "Title") @Html.TextBoxFor(m => m.Title, new { @class = "text-input" }) </li> <li class="form-row text-input-row name-field"> @Html.LabelFor(l => l.VideoUrl, "Video Url") @Html.TextBoxFor(m => m.VideoUrl, new { @class = "formfield" }) </li> <li class="form-row text-input-row name-field"> @Html.LabelFor(l => l.StarRating, "Star Rating") @Html.HiddenFor(m => m.StarRating) <div id="star-rating-slider" class="star-rating-field"> <div class="star-rating-value-field"></div> </div> </li> <li class="form-row text-area-row name-field"> @Html.LabelFor(l => l.Description, "Description") @Html.TextAreaFor(m => m.Description) </li> <li class="form-row text-input-row name-field"> @Html.LabelFor(l => l.DateString, "Date") <div class="date-picker-container"> @Html.TextBoxFor(m => m.DateString, new { @class = "formfield date-picker" }) </div> </li> <li class="button-row"> <input type="submit" name="Submit" value="Submit" class="btn btn-submit bm0" /> </li> </ol> </fieldset> } </div>
The BeginUmbracoForm call automatically renders an HTML form and tells Umbraco to use the CinemaCentoController and call the “Submit” function when the form is submitted. This allows us to use the @Html helper functions to create the fields within the form and link them to the model's attributes (e.g. TextBoxFor, HiddenFor).
The AntiForgeryToken call is included as an extra security precaution, it helps to stop other pages attacking the site, by checking that the form was created by my server.
For the slider (for the star rating) and date picker I used the JQuery UI library. These required a bit of extra JavaScript to initialise the controls and to store the values in the correct fields when they change.
/*-----------------------------------------------------------------------------------*/ /* Cinema Cento Form /*-----------------------------------------------------------------------------------*/ $(document).ready(function () { var initialStarRating = 4; $("#star-rating-slider").slider({ min: 0, max: 5, step: 0.5, value: initialStarRating, slide: function (event, ui) { $(".star-rating-value-field").text(ui.value); $("#StarRating").val(ui.value); } }); $(".star-rating-value-field").text(initialStarRating); $("#StarRating").val(initialStarRating); $(".date-picker").datepicker({ autoSize: true, showOn: "button", buttonImage: "/style/css/images/calendar.jpg", buttonImageOnly: true, buttonText: "Select date", dateFormat: "dd/mm/yy" }); });
I also needed a bit of CSS for some of the layout of these controls – particularly the positioning of the calendar image button on the date control. I won’t include that here, as I’m no expert and I suggest you seek a more informed source!
Finally, the controller contains the code that gets fired when the form is submitted. Here I’m using some of the helper functions that I wrote before to create Umbraco content, which in turn creates the Tumblr post using the API (as described in previous posts).
ViewData is used to return error information when the form cannot be validated. Note that this can only be used when CurrentUmbracoPage() is called to remain on the current page with the fields still populated. If you wish to redirect to this or another page then you would need to use TempData as that survives the redirect process.
using rtmullinger2018.Helpers; using rtmullinger2018.Models; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Web.Mvc; using TumblrApi.Model; using Umbraco.Core.Models; using Umbraco.Web.Mvc; namespace rtmullinger2018.Controllers { public class CinemaCentoController : SurfaceController { [HttpPost] [ValidateAntiForgeryToken] public ActionResult Submit(CinemaCentoForm model) { ViewData["cinema-cento-form.error"] = ""; if (!ModelState.IsValid) { ViewData["cinema-cento-form.error"] = "There was a problem with the field validation."; return CurrentUmbracoPage(); } Post post = new Post(); CinemaCentoPostHtml htmlInfo = new CinemaCentoPostHtml(); String error = ValidateForm(model, post, htmlInfo); if (!String.IsNullOrWhiteSpace(error)) { ViewData["cinema-cento-form.error"] = error; return CurrentUmbracoPage(); } TumblrHelper helper = new TumblrHelper(ApplicationContext); IPublishedContent cinemaCentoRoot = helper.GetRoot(); if (cinemaCentoRoot != null) { IPublishedContent parentFolder = helper.GetParentFolder(cinemaCentoRoot, post.date); if (parentFolder != null) { List<String> tags = new List<String>(); tags.Add(post.title.ToLower()); post.tags = tags.ToArray(); error = helper.CreateNewNode(parentFolder, post, htmlInfo); if (!String.IsNullOrWhiteSpace(error)) { ViewData["cinema-cento-form.error"] = error; return CurrentUmbracoPage(); } } else { ViewData["cinema-cento-form.error"] = "Could not find or create parent folder for post."; return CurrentUmbracoPage(); } } else { ViewData["cinema-cento-form.error"] = "Could not find root folder for posts."; return CurrentUmbracoPage(); } IPublishedContent thanksPage = null; IPublishedContent miscPages = Umbraco.TypedContentAtRoot().FirstOrDefault(r => r.DocumentTypeAlias == "miscellaneousPages"); if (miscPages != null) { thanksPage = miscPages.Children.FirstOrDefault(p => p.Name == "Cinema Cento Thanks"); } if (thanksPage == null) { thanksPage = Umbraco.TypedContentAtRoot().FirstOrDefault(p => p.DocumentTypeAlias == "homePage"); } return RedirectToUmbracoPage(thanksPage); } private String ValidateForm(CinemaCentoForm model, Post post, CinemaCentoPostHtml htmlInfo) { String error = ""; if (!String.IsNullOrWhiteSpace(model.Title)) { post.title = model.Title; } else { error = "Please supply a title."; } if (!String.IsNullOrWhiteSpace(model.Description)) { htmlInfo.Text = model.Description; } else { error = "Please supply a description."; } htmlInfo.YouTubeUrl = model.VideoUrl; if (!String.IsNullOrWhiteSpace(model.Title)) { post.title = model.Title; } else { error = "Please supply a title."; } DateTime postDate; if (DateTime.TryParseExact(model.DateString, CinemaCentoForm.DATE_FORMAT, CultureInfo.InvariantCulture, DateTimeStyles.None, out postDate)) { post.date = postDate; } else { error = "Could not parse the date: " + model.DateString; } if (model.StarRating >= 0.0f && model.StarRating <= 5.0f) { htmlInfo.StarRating = model.StarRating; } else { error = "Unexpected star rating value: " + model.StarRating; } return error; } } }
If the post is successfully submitted then the controller redirects to a specially created "thank you" page.
To include my new form in my LeBlender grid editor, it’s simply a matter of creating the editor code as a file under /Views/Partials/Grid/Editors/CinemaCentoForm.cshtml.
@using rtmullinger2018.Models @{ Html.RenderPartial("~/Views/Partials/_CinemaCentoForm.cshtml", new CinemaCentoForm()); }
Finally, I created a page and added the new editor to the grid on that page. Here are the results.