📜 ⬆️ ⬇️

Simple way to create complex ASP.NET MVC controls

Surely everyone created their asp.net mvc controls (this is, of course, about asp.net mvc coders). Should you be familiar with the method for creating controls using TagBuilder ? Tried to write really complex controls (for example, with a large number of javascript or markup, which depends on the options)? Then surely you are familiar with ad escaping quotes, string concatenation (or calling the .Format () function), etc. "Inconvenience." I propose to look at a fairly simple technique that will avoid such things and at the same time focus on the functional side of the controls, and not on the programming of the husk.

Denial of responsibility


Much of what will be said here may turn out to be a wild accordion and govnokodom. In addition, this post is designed to be quite puzzled by the problems of the people described in the first paragraph. This is not a tutorial, and not a guide to action. This is only a description of the campaign, and no more than.

Classic creation of ASP.NET MVC controls


Let's briefly go over how the controls are now molded.
Usually do HtmlHelper extension like this:
public static string InputExControl(this HtmlHelper @this) { StringBuilder sb = new StringBuilder(); sb.Append("<input type=\"text\">.......blablabla.........."); return sb.ToString(); } 

or use TagBuilder for the same purpose:
 public static string InputExControl(this HtmlHelper @this) { TagBuilder tagBuilder = new TagBuilder("input"); //.....blablabla.... return tagBuilder.ToString(); 


Personally, it turns me up from this code: at least this code is difficult to maintain (IMHO), and, moreover, it's not easy to modify it either.

Task


Let's set such a task: we need to make the input control with AJAX validation on the server side (based on the specified Regexp), an indication of the validation result, and so that the control is in a separate assembly. This is a simple control that will show the general idea.
')

How do others do it?


To begin, consider a few libraries, similar in functionality, to understand how they do it:


Other commercial controls mainly use the “StringBuilder.Format ()” approach (for pure asp.net mvc controls), but may (like DevExpress) pull asp.net webForms controls.

Idea


The idea is to use the asp.net mvc partial view asp.net syntax to describe the control in .cshtml, but without dragging the .cshtml files along, of course.

Let's create a simple asp.net mvc 4 project in the studio, add to the library solution (let's call it MyControlLib), in which we will reference the main asp.net mvc 4 libs.

Add the InputExControl.cshtml file with empty content to MyControlLib. This will be our View-part of the control that we previously wrote using the TagBuilder technique. In order to translate this cshtml file into C # code, we will use the Razor Generator . It will allow us to generate what Razor engines would generate us on the fly in an asp.net mvc application. It is necessary as a matter of fact in order not to drag .cshtml files behind you and appear as an “adult control” (commercial ones don’t pull, so we won’t become). Okay, let's put the following in InputExControl.cshtml contents:
 @* Generator: MvcView *@ 

, and in its properties we specify the Custom Tool as the RazorGenerator.

Received a derived class derived from System.Web.Mvc.WebViewPage. The generation is not very convenient, because creates a non-partial class (you can fix it in the source code of the generator or simply by hand in the generated file), i.e. such a class is hard to expand using the methods we need.

Let's create the class InputExControlSettings , which will personify the settings of our control. It will be very simple:
  public class InputExControlSettings { public string Name { get; set; } //  public dynamic CallbackRouteValues { get; set; } //  ajax callback- public string ValidationRegexp { get; set; } //   } 

For simplicity, I started the Name field, which will impersonate its id in the client code (the property of the DOM element). Also this field will participate in the generation of the names of the sub-elements needed by our control (validation result indicator).

We will need the CallbackRouteValues property to get Uri where we send a validation request from client javascript. It usually indicates the controller and controller method.

Now the settings class can be specified as a model for our control-cshtml file:
 @* Generator: MvcView *@ @model InputExControlSettings @{ //     ,       string controlId = Model.Name; string controlResultId = Model.Name + "_Result"; } 


In order to write the code, we need to understand one simple thing: the callback will generally pull our control, therefore we need to somehow distinguish the callback from a simple GET request to get the look of the control. For us, by a simple method of definition, I chose the presence in the headers of a request of “special” (our) value. So we got a small helper. In addition, I used it (bad code!) As an assistant to get Uri from CallbackRouteValues values:
  internal static class InputExControlHelper { public static bool IsCallback() { return !string.IsNullOrEmpty(HttpContext.Current.Request.Headers["InputExControAjaxRequest"]); } public static MvcHtmlString CallbackHeaderName { get { return MvcHtmlString.Create("InputExControAjaxRequest"); } } public static string GetUrl(dynamic routeValues) { if (HttpContext.Current == null) throw new InvalidOperationException("no context"); RequestContext context; if (HttpContext.Current.Handler is MvcHandler) { context = ((MvcHandler) HttpContext.Current.Handler).RequestContext; } else { var httpContext = new HttpContextWrapper(HttpContext.Current); context = new RequestContext(httpContext, new RouteData()); } var helper = new UrlHelper(context, RouteTable.Routes); return helper.RouteUrl(string.Empty, new RouteValueDictionary(routeValues)); } } 


Okay, it's time to write the code for the presentation of our control:
view cshtml code
 @* Generator: MvcView *@ @model InputExControlSettings @{ string controlId = Model.Name; string controlResultId = Model.Name + "_Result"; } @if(!InputExControlHelper.IsCallback()) { <input type="text" id="@controlId"/> <span id="@controlResultId"></span> <script> $(function() { $('#@controlId').change(function () { $('#@controlResultId').text('validating ...'); $.ajax({ url: '@InputExControlHelper.GetUrl(Model.CallbackRouteValues)', headers: { '@InputExControlHelper.CallbackHeaderName': true }, cache: false, data: { value: $('#@controlId').val() }, type: 'POST', dataType: 'json', success: function (data) { if (data) { $('#@controlResultId').text('Validattion result: ' + data.result); } else { alert('result error?'); } }, error: function() { alert('ajax error'); } }); }); }); </script> } else { System.Web.HttpContext.Current.Response.ContentType = "application/json"; @(this.InternalValidate(System.Web.HttpContext.Current.Request.Form["value"])) } 



The code is as simple as possible: if we don’t have a callback, then we display the main View, including javascript, which will do the same callback. In the case of callback, we set ContentType as JSON and call the InternalValidate(string) control validation method.

Actually, the code itself validation and installation of ViewData.Model will be designed as a partial InputExControl method and will be very simple:
 partial class InputExControl { public InputExControl(InputExControlSettings settings) { ViewData.Model = settings; } private MvcHtmlString InternalValidate(string value) { Thread.Sleep(2000); //long validation emulator... var settings = ViewData.Model; var regexp = new Regex(settings.ValidationRegexp, RegexOptions.Compiled); var res = regexp.IsMatch(value); var scriptSerializer = new JavaScriptSerializer(); var rv = scriptSerializer.Serialize(new { result = res }); return MvcHtmlString.Create(rv); } } 


Ok, we wrote a control, but we can't use it in our MVC project yet. The real boys write the HtmlHelper extender for such controls, which we will do:
 namespace MyControlLib { public static class HtmlExtensions { public static HtmlString InputEx(this HtmlHelper @this, Action<InputExControlSettings> setupFn) { var options = new InputExControlSettings(); //   setupFn(options); //  var view = new InputExControl(options); //   var tempWriter = new StringWriter(CultureInfo.InvariantCulture); //       Razor view.PushContext(new WebPageContext(), tempWriter); //   view.Execute(); //  View -     view.PopContext(); //   return MvcHtmlString.Create(tempWriter.GetStringBuilder().ToString()); //    View } } } 


Okkay, we now have an expander method. It's time to integrate our control into the main application. Simply create a PartialView MyInputCtrlPartial (and an Action method named as well), where enter something like
 @using MyControlLib @Html.InputEx(s=> { s.Name = "MyInputCtrl"; s.CallbackRouteValues = new { Controller = "Home", Action = "MyInputCtrlPartial" }; s.ValidationRegexp = @"^\d+$"; }) 


and call it (using Html .Partial ("MyInputCtrlPartial")) in the main View.

We need to describe the control in PartialView, since the result of the "rendering" will be different - depending on the transferred header indicator that we have a callback to check.

It remains only to launch the project for execution and make sure that everything works (or does not work, because someone has nakosyachil) (note: in order to trigger the changed event, you need to poke past the control with the mouse).

Total


Bottom line: we were able to write a difficult control, while Intellisense works in the Razor template (including javascript), which is good news.

A sample project can be downloaded from http://rghost.ru/42818685 ( mirror ).

Comments are welcome.

Source: https://habr.com/ru/post/165025/


All Articles