FluentValidation

by MikeHogg 29. March 2013 20:56

I've always been kind of disappointed in my model class files in MVC after the first or second phase of a project, because the properties end up with so many DataAnnotation attributes that it's just easier to read an intellisense popup to see the class properties than view the code file directly.  And this is just with simple Display and Validation Attributes.

DisplayAttributes always seemed wrong to me just on principle.  It is putting Presentation Layer stuff in Domain Model files.  I always have to show the front end developers where this ‘default’ display attribute can be changed, and they are not used to looking at or modifying c# code.  For this reason, I’m torn on whether to continue to use Display attributes automatically.  But to help simplify my Model files, I would rather pull what model properties I need for my particular view out into ViewModel files, and put the attributes there. 

Validation Attributes are also tied to the Presentation, or User Input Layer.  Several times it happens to me that one page requires some validation, and another page using the same model, does not require validation (usually because it’s in read-only state on that page) and then I have to work around the validators.  These also belong in VM files.  So your Required Validators are only called where they are used.  Now your Model classes are clean, and should pretty closely resemble your data objects.

But- Validation isn’t strictly front end or presentation layer stuff, and so for further simplification, jump on the Fluent bandwagon, and take all those Validation attributes out of your ViewModel files and put them in FluentValidator classes.  It's really easy to set up, and the benefits you get are similar to Activity Based Authorization, in that all of your validation stuff is in exactly one place, and isn't interlaced in the display code of dozens of other presentation files.  Your Validation is now loosely coupled to both your presentation layer and your domain layer, and your code is easier to read and maintain.

Add the FluentValidationMVC to your NuGet, and let it pull in the dependencies.  Add exactly one line to your Application_Start

FluentValidation.Mvc.FluentValidationModelValidatorProvider.Configure();

and then put the attribute on your VM class itself,

[FluentValidation.Attributes.Validator(typeof(QuestionOptionValidator))]

And then create the validator class, and you can just enter all the validations, and nothing but the validations...

    public class QuestionOptionValidator : FluentValidation.AbstractValidator<QuestionOption>
    {
        public QuestionOptionValidator()
        {
            RuleFor(x => x.QuestionID).GreaterThan(0).When(qo => qo.ID != 0);
            RuleFor(x => x.ChartDisplay).NotNull();
            RuleFor(x => x.SpanishChartDisplay).NotEmpty().WithMessage("Please enter a Spanish Chart");
            RuleFor(x => x.CurrentlyViewedEventID).Must((q, currentlyviewedeventid) => q.IsActive && currentlyviewedeventid > 0);
            RuleFor(x => x.Description).Matches("^Desc.*$");
 
        }
         
    }

I don't need to paste examples, their docs are great.

http://fluentvalidation.codeplex.com/wikipage?title=Validators&referringTitle=Documentation&ANCHOR#Predicate

Tags:

MVC | Fluent

MVC Routing fun

by MikeHogg 22. October 2012 10:08

I had a very small landing page project request for implementing a few thousand personalized URLs.  From what I knew about MVC routing, I figured it would be an easy task, and it was, but also a nice sample of the power of the new routing engine.

 

public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
            routes.Add("purls", new CustomRoute(
                "{id}", 
                new RouteValueDictionary(new { controller = "Home", action = "getquerystring" }),  // the default will send it to get querystring, and the override GetRouteData will then send it to purl
                new { id = new ActionNameConstraint()} // only if Action not exist
                ));   
            
            routes.MapRoute(
                "NoHome", // set default controller to Home so no /Home/ in url
                "{action}",  
                new { controller = "Home", action = "Index" } 
            );
        }

 

This has two extensible features of the MVC routing system- a custom route, and a custom constraint class, rather than just a constraint object.   The custom route is to prepare the purl action, and the constraint is to skip to the next route if an action name already exists for that route (static route).  I wanted to load an array of action names on Application_Startup, but apparently you can no longer access RequestContext that soon in the lifecycle, so I just implemented a lazy readonly Getter for an Application variable like so:

 

public class GLOBAL
    {
     public static string[] HomeActions
        {
            get
            {
                if (_homeactions == null)
                {
                    var controller = ControllerBuilder.Current.GetControllerFactory().CreateController(HttpContext.Current.Request.RequestContext, "Home");
                    var controllerType = controller.GetType();
                    var controllerDescriptor = new ReflectedControllerDescriptor(controllerType);
                    _homeactions = controllerDescriptor.GetCanonicalActions().Select(a => a.ActionName).ToArray();
                }
                return _homeactions;
            }
        }
 

 

And then, my constraint just needs to check against the array-

 

 

 
public class ActionNameConstraint : IRouteConstraint
    {
        public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
        {
            if (values.ContainsKey(parameterName))
            {
                string stringValue = values[parameterName] as string;
                return !GLOBAL.HomeActions.Contains(stringValue);
            }
            return false;
        }
    }
 

 

And, if it passes the constraint, our first stop is an action called GetQueryString, to append some utm= keyvalue  pairs for our google analytics, which then redirects to the purl lookup action...

 

 
public ActionResult GetQueryString(string id)
        { 
            Dictionary<string,object> d = new Dictionary<string,object>();
            GLOBAL.QUERYSTRINGOBJECT.CopyTo(d);
            return RedirectToAction(id, new RouteValueDictionary(d));
            
        }
...
public class GLOBAL
    {
        public static System.Collections.Specialized.NameValueCollection QUERYSTRINGOBJECT = HttpUtility.ParseQueryString("utm_source=DM&utm_medium=something&utm_campaign=something"); 

... and that's where our Custom route comes in, simply to check for existance of a querystring, and if so, then changes the action to "purl"...

 

 public class CustomRoute : Route
    {
        public CustomRoute(string url, RouteValueDictionary routevalues, object constraints) : base(url, routevalues, new RouteValueDictionary( constraints ), new MvcRouteHandler()) {}
 
        public override RouteData GetRouteData(HttpContextBase httpContext)
        { 
            RouteData routeData = base.GetRouteData(httpContext);
            if (routeData == null) return null;
             
            if (httpContext.Request.QueryString.Keys.Count > 0) routeData.Values["action"] = "purl";
            return routeData;
        }         
    } 

 

The "purl" action is going to do the lookup against 10k urls for this user's viewname, all the while keeping the url that the user typed in their address bar intact, with the addition of the GA querystring.

 

 

 
public ActionResult Purl(string id)  
        {
            Models.PModel pmodel = lib.Repo.GetPModel(id);
 
            if (String.IsNullOrEmpty(pmodel.ViewName)) return RedirectToAction("NotFound");
 
            return View(pmodel.ViewName, pmodel);
        }
 

 

 

And that's it.

Tags:

MVC

A More Mature User (model)

by MikeHogg 12. August 2012 09:52

My MVVM and MVC User models have usually been a hierarchy of different classes starting with the simplest name/password and adding more properties with each inherited subclass. 

 

(Side Note: I've noticed a tendency to add more properties to different versions of subclasses can get out of hand, mucking up a membership provider, when mixed with Roles, when what is really called for is a Profile provider for all those descriptive properties.  See Decorate Pattern and MS ProfileProvider.)

 

 

 

This time a client requirement surprised me by making even username/password optional, which I've never done.  As a matter of fact, most of my user hierarchy was built to support the base class of required properties using MVC DataAnnotation Required Attributes.  So

 

I rewrote it as one base User with no requireds, and then various subclasses more like ViewModels.  Also I Interfaced the User and changed all my Membership and Repository arguments to the interface.  Now this pattern seems much more flexible, simple, and easily extensible.  Don't know why I didn't see this before.

 

 

public interface IUserModel
    {
        int Id { get; set; }
 
        string EmailAddress { get; set; }
        string Password { get; set; }  
 
        bool Active { get; set; }
        string FirstName { get; set; }
        string LastName { get; set; }
        string PhoneNumber { get; set; }
        bool GetsEmail { get; set; }
 
        IEnumerable<RoleModel> Roles { get; set; }
    }
    
    [Serializable]
    public class UserModel : IUserModel
    {
        [Key]
        public int Id { get; set; }
 
        [Display(Name = "Email Address")]
        [StringLength(255)]
        [MyLibrary.Web.Mvc3.Attributes.EmailAddress]
        public virtual string EmailAddress { get; set; }
 
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public virtual string Password { get; set; }
 
        [Display(Name = "Active")]
        public bool Active { get; set; }
 
        [Display(Name = "First name")]
        [StringLength(50)]
        public virtual string FirstName { get; set; }
 
        [Display(Name = "Last name")]
        [StringLength(50)]
        public string LastName { get; set; }
 
        [StringLength(255)]
        [Display(Name = "Phone Number")]
        public string PhoneNumber { get; set; }
 
        [Display(Name = "Gets Emails?")]
        public bool GetsEmail { get; set; }
 
        [Display(Name = "Roles")]
        public IEnumerable<RoleModel> Roles { get; set; }
    }
 
 
    public class LogOnModel : UserModel
    {
        [Required]
        [Display(Name = "Email Address")]
        [StringLength(255)]
        [MyLibrary.Web.Mvc3.Attributes.EmailAddress]
        public override string EmailAddress { get; set; }
 
        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public override string Password { get; set; }
 
    }
 
    public class RegisterModel : LogOnModel
    { 
        [Required(ErrorMessage = "Please enter a first name")]
        public override string FirstName { get; set; }
 
        [DataType(DataType.Password)]
        [Compare("Password")]
        [Display(Name = "Confirm Password")]
        public string PasswordConfirm { get; set; }
    }
 
    public class ChangePasswordModel : UserModel
    {
        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Old Password")]
        public string OldPassword { get; set; }
        
        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public override string Password { get; set; }
 
        [DataType(DataType.Password)]
        [Compare("Password")]
        [Display(Name = "Confirm Password")]
        public string PasswordConfirm { get; set; }
    }
 
    public class FoundPasswordModel : UserModel
    {
        [Required] 
        public override string EmailAddress { get; set; }
 
        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public override string Password { get; set; }
 
        [DataType(DataType.Password)]
        [Compare("Password")]
        [Display(Name = "Confirm Password")]
        public string PasswordConfirm { get; set; }
    }
 
    public class ValidatedUserModel : UserModel
    {
        [Required]        [StringLength(255)]
        [MyLibrary.Web.Mvc3.Attributes.EmailAddress]        public override string EmailAddress { get; set; }
 
        [Required(ErrorMessage = "Please enter a first name")]
        public override string FirstName { get; set; }
 
        public ValidatedUserModel() { }
        public ValidatedUserModel(IUserModel user)
        {
            Active = user.Active;
            EmailAddress = user.EmailAddress;
            FirstName = user.FirstName;
            GetsEmail = user.GetsEmail;
            Id = user.Id;
            LastName = user.LastName;
            PhoneNumber = user.PhoneNumber;
            Roles = user.Roles;
        }
    }

Tags:

MVC | Architecture

Best of Datagrid plugins for web

by MikeHogg 12. August 2012 09:43

After researching several datagrid mechanisms for mvc3 web page, looking for powerfule filtering and sorting and paging built in, I went with actually a javascript implementation called SlickGrid.

 

https://github.com/mleibman/SlickGrid

 

There was another js grid actually that got very positive reviews also.  I think there was a SO question asking for reviews but it's been a while since I did this research and I don't remember the name or why I chose this one over the others, or over .net controls.  I just want to document how I used it for the future. It offers powerfully fast sorting paging and even As You Type filtering on datasets upwards of 100k I am told, and believe, although I have not had need to use it for more than an order of hundreds yet.

 

besides adding the source (i use slick.core, .dataview, .formatters (might be mine), and .pager) to your project, you init the grid like any other plugin.  You set your Columns and options.  Columns here is the important point.

 

 
<link href="@Url.Content("~/Content/slick.grid.css")" rel="stylesheet" type="text/css" />
<link href="@Url.Content("~/Content/slick.pager.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Content/js/jquery.event.drag-2.0.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Content/js/slick.core.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Content/js/slick.grid.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Content/js/slick.dataview.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Content/js/slick.formatters.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Content/js/slick.pager.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Content/js/json2.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Content/js/slick.mylib.js")" type="text/javascript"></script>
 
<script type="text/javascript">
 
    var columns = [
     { id: "Name", name: "Name", field: "Name", width: 140, sortable: true },
                    { id: "Email", name: "Email", field: "Email", width: 140, sortable: true }, 
     { id: "Type", name: "Type", field: "Type", sortable: true }, 
                    { id: "Title", name: "Title", field: "Title", width: 150, sortable: true },
                    { id: "RequestDate", name: "Request Date", field: "RequestDate", formatter: Slick.Formatters.Date, filter: false, sortable: true },
                    { id: "DueDate", name: "Due Date", field: "DueDate", formatter: Slick.Formatters.Date, filter: false, sortable: true },
                    { id: "AnotherDueDate", name: "Another Due Date", field: "AnotherDueDate", formatter: Slick.Formatters.Date, filter: false, sortable: true },
                    { id: "Details", name: "Details", field: "Id", filter: false,
                        formatter: function (row, cell, value, columnDef, dataContext) {
                            return '<a href="/Home/ViewDetails/' + dataContext['Id'] + '">Details</a>';  }   },
 
                    { id: "selector", name: "Has Approval", field: "HasApproval", formatter: Slick.Formatters.Checkmark, cssClass: "centered", filter: false, sortable: true },
                    { id: "HasDeadline", name: "Has Deadline", field: "HasDeadline", formatter: Slick.Formatters.Checkmark, cssClass: "centered", filter: false, sortable: true },
                    { id: "AddFiles", name: "Add Files", field: "HtmlUploads", cssClass: "icons centered", filter: false,
                        formatter: function (row, cell, value, columnDef, dataContext) {
                            return '<span class="add-files" data-for="#add-files-text-' + dataContext['Id'] + '">Add</span>' +
                            '<span id="add-files-text-' + dataContext["Id"] + '" class="add-files-text">' +
                            value + '</span>'; }   },
 
                    { id: "HasCapital", name: "Has Capital", field: "HasCapital", cssClass: "centered", filter: false, sortable: true, formatter: Slick.Formatters.Checkmark },
                    { id: "DownloadFiles", name: "Download", field: "HtmlDownloads", width: 80, filter: false, formatter: function (row, cell, value, columnDef, dataContext) {
                        return '' + value; }   }
                ];
 
 
                var options = {
                    enableColumnReorder: false,
                    forceFitColumns: true,
                    defaultColumnWidth: 60,
                    autoHeight: true,
                    rowHeight: 60,
                    showHeaderRow: true,
                    headerRowHeight: 37
                };
 
</script>   
 
<h2>Welcome to the Mike Hogg Something Manager</h2>
 
<div class="text clearfix">
    <div class="button-cluster centered wide clearfix">
        @Html.ActionLink("Submit New Something", "NewSomething", null, null, new { @class = "btn" })
        @Html.ActionLink("View Calendar", "Index", "Calendar", null, new { @class = "btn" })
    </div>
    <br />
    <p>Below is a list of submitted somethings currently in your queue. To upload a xyz or abc file, select your something and click on the "Add File" icon. You can also sort the somethings by clicking on the column header, or filter by typing in  stuff</p>
</div> 
 
    <input type="hidden" id="message" value="used in getExport() above"/>
    <div id="myGrid"></div> 
    <div id="myPager"></div> 

 

And then in your js you set up your ajax method to get the json that populates the column, and sets the data to a placeholder control on your page.  I think leiberman builds the sort event into the grid, but you need to write your own comparers, so mine is here.  I think the basic filter was included but needs to be extended for your needs.  I had a need for escaping HTML and found the escape map also on S.O.

 

 

 
var grid;
var sortcol = "RequestDate";
var sortdir = -1;
var columnFilters = {};
var dataView = new Slick.Data.DataView({ inlineFilters: true });
dataView.setPagingOptions({ pageSize: 8 });
 
 
// my freshmen filter
function filter(item) {
    for (var columnId in columnFilters) {
        if (columnId !== undefined && columnFilters[columnId] !== "") {
            var c = grid.getColumns()[grid.getColumnIndex(columnId)];
            //if (item[c.field] != columnFilters[columnId]) {
 
            var field = item[c.field], myfilter = columnFilters[columnId];
            if (field != null && myfilter != null) {
                if (field.toString().toLowerCase().indexOf(myfilter.toString().toLowerCase()) == -1) {
                    return false;
                }
            } else { return field == myfilter; }
        }
    }
    return true;
}
 
// my freshmen attempt at comparer (for SORT)
function comparer(a, b) {
    var x = isNaN(a[sortcol]) ? a[sortcol].toLowerCase() : a[sortcol];
    var y = isNaN(b[sortcol]) ? b[sortcol].toLowerCase() : b[sortcol];
    if (x == null || x == "") {
        if (y == null || y == "") {
            return 0;
        }
        else return -1;
    } 
    else if (y == null || y == "") return 1;
    else return (x == y ? 0 : (x > y ? 1 : -1));
}
 
 
 
var entityMap = {
    "&": "&amp;",
    "<": "&lt;",
    ">": "&gt;",
    '"': '&quot;',
    "'": '&#39;',
    "/": '&#x2F;'
};
 
function escapeHtml(string) {
    return String(string).replace(/[&<>"'\/]/g, function (s) {
        return entityMap[s];
    });
}
/// ADDITIONAL

 

We had some cute image links for a popup menu in one of the cells but as you scroll and page through the grid, they needed to be recreated as this grid implementation actually dropped and added rows on and off the dom on the fly, so LoadButtons ...

 

 
// for those pencil icon popup menus for Add Files
function loadButtons() {
 
    $(".add-files-text").dialog({
        autoOpen: false,
        title: "Add Files"
    });
    $(".add-files").button({
        icons: { primary: 'ui-icon-pencil' },
        text: false
    });
    $(".add-files").click(function () {
        $($(this).attr("data-for")).dialog("open");
        return false;
    });
}
 
 
// adds those filters to each column, special here to not add for certain columns, extended by 'filter:'
function updateHeaderRow() {
    for (var i = 0; i < columns.length; i++) {
        var header = grid.getHeaderRowColumn(columns[i].id);
        $(header).empty();
        if (columns[i].id !== "selector" && columns[i].filter != false) {
            $("<input type='text'>")
                    .data("columnId", columns[i].id)
                    .val(columnFilters[columns[i].id])
                    .appendTo(header);
        } else {
            $("<div><span style='height:39px;display:block;'></span></div>").appendTo(header);
        }
    }
    if (grid.getOptions()["excelExport"] == true) {
        // add Excel button
        var header = grid.getHeaderRowColumn(columns[columns.length - 1].id);
        $(header).empty();
        $("<input type='image' src='../Content/img/Excel-icon.png' title='Export to Excel' onclick='getExport();'/>").appendTo(header);
    }
}
 
 
 
function getExport() {
    $.ajax({
        url: '/Home/DeploymentList',
        type: "post",
        data: JSON.stringify(dataView.getFilteredRows()),
        dataType: "json",
        contentType: "application/json; charset=utf-8",
        success: function (result) { getCSVFileFromFormPost(result); },  // ajax post json data which server can read automatically, responds with a string, which ajax then POSTs inside a FORM, to which the server adds headers and returns, so browser can treat it as a download (save/open)
        error: function (xhr, textStatus, errorThrown) {
            $("#message").html('Error occurred, ReadyState: ' + xhr.readyState +
                '; textStatus: ' + textStatus + '; ' + errorThrown);
        }
    });
    return false;
}
 
function getCSVFileFromFormPost(result) {
    $('<form action="/Home/GetCSVFile" method="post"><input type="hidden" name="csv" id="csv" value="' + escapeHtml(result) + '" /></form>').appendTo("body").submit();
}
 

 

The Export to CSV function was a neat one, since we could send the dataset back to the server in json easily, and in MVC we could set up an Action to Deserialize that same json to a List of Models if we lined everything up right.

 

And "the call"...

 

$(function () {
    $.post('/Home/GetDepData', function (data, textstatus) {
 
        grid = new Slick.Grid("#myGrid", dataView, columns, options);
        var pager = new Slick.Controls.Pager(dataView, grid, $("#myPager"));
 
        grid.onSort.subscribe(function (e, args) {
            sortdir = args.sortAsc ? 1 : -1;
            sortcol = args.sortCol.field;
 
            if ($.browser.msie && $.browser.version <= 8) {
                // using temporary Object.prototype.toString override
                // more limited and does lexicographic sort only by default, but can be much faster
 
                dataView.fastSort(sortcol, args.sortAsc);
            } else {
                // using native sort with comparer
                // preferred method but can be very slow in IE with huge datasets 
                dataView.sort(comparer, args.sortAsc);
            }
        });
 
        // wire up model events to drive the grid
        dataView.onRowCountChanged.subscribe(function (e, args) {
            grid.updateRowCount();
            grid.render();
 
            loadButtons();  // when filtered to fewer rows than before and no rows were changed
        });
 
        dataView.onRowsChanged.subscribe(function (e, args) {
            grid.invalidateRows(args.rows);
            grid.render();
 
            loadButtons();  // when filtered or paged
        });
 
        dataView.onPagingInfoChanged.subscribe(function (e, pagingInfo) {
            var isLastPage = pagingInfo.pageNum == pagingInfo.totalPages - 1;
            var enableAddRow = isLastPage || pagingInfo.pageSize == 0;
            var options = grid.getOptions();
        });
 
 
        $(grid.getHeaderRow()).delegate(":input", "change keyup", function (e) {
            columnFilters[$(this).data("columnId")] = $.trim($(this).val());
            dataView.refresh();
        });
 
        // initialize the model after all the events have been hooked up
        dataView.beginUpdate();
        dataView.setItems(data, "Id");  // Id capital I to match my unique model id property
        dataView.setFilter(filter);
        dataView.endUpdate();
 
        updateHeaderRow();
        loadButtons();
    }, "json");
});

 

At the end of the included slick.formatters.js I added a couple of my own, the javascript time function being a keeper...

 

    ...
    function YesNoFormatter(row, cell, value, columnDef, dataContext) {
        return value ? "Yes" : "No";
    }
 
    function CheckmarkFormatter(row, cell, value, columnDef, dataContext) {
        return value ? "<img src='../Content/img/slickimages/tick.png'>" : "";
    }
    function DateFormatter(row, cell, value, columnDef, dataContext) {
        if (value !== null) {
            var d = new Date(parseInt(value.substr(6, 13)));
            return (d.getMonth() + 1) + "/" + d.getDate() + "/" + d.getFullYear() + ' ' + getAMPMTime(d);
        } else return "";
    }
 
    Number.prototype.pad = function (len) {
        return (new Array(len + 1).join("0") + this).slice(-len);
    }
 
    function getAMPMTime(d) {
        var d = new Date(d);
        var hour = d.getHours();
        var min = d.getMinutes().pad(2);
        var ap = "AM";
        if (hour > 11) { ap = "PM"; }
        if (hour > 12) { hour = hour - 12; }
        if (hour == 0) { hour = 12; }
 
        if (hour == 12 && min == 0 && ap == "AM") return '';
 
        return hour + ':' + min + ' ' + ap;
    }

Tags:

JQuery | MVC | Javascript

CheckboxLists in MVC3

by MikeHogg 2. August 2012 09:48

 

MVC does not really model bind checkboxes. The problem is html based; input checkboxes do not post in the form unless actually checked. MVC gets around this with their helper by creating a hidden for every checkbox, and I assume unobstrusively sets the hidden input whenever the associated checkbox is toggled.

 

This means that we cannot validate the checkbox as required, which isn't an easy case anyway since a bool defaults to false. It’s not a nullable type.  And we can't readily access the hidden input to validate that.  People have created several roll-your-own extensions and helpers out there… but I couldn’t find one that fit my needs exactly.

 

My main requirement was a list of boxes (a grouping so to speak) requiring at least one to be checked. With client validation (most examples on the web missed this one).  SO I found myself a little library project.

 

Two parts to this-

  • - [In your Model]: add your list of checkboxes like this: IEnumerable<MH.Mvc.Checkbox> { new {Name="Bank"} new {Name="Stocks", IsChecked?="true"}}
    • decorate your checkboxlist with [MH.Attributes.CheckBoxListServerValidator], and you will get server validation that at least one box is checked
    • In your View, use the HtmlHelper.CheckBoxListFor(m=>m.checkboxlist) which gives you the proper model binding
  • - [In your Model]: add a bool named something like this: CheckboxClientValidator
    • decorate your bool with [MH.Attributes.CheckBoxListClientValidator(listname)], and
    • add the MH/Content/scripts/custom-validators js to your page and you will get client side validation that at least one box is checked

Here is what my Model looks like:

 

 

[MH.Web.Mvc3.Attributes.CheckBoxListClientValidator("LiquidAssetSourceList")]
        public bool LiquidAssetClientValidator { get; set; }    
        [Display(Name = "Where do you have your liquid assets? (Check all that apply.)*")]
        [MH.Web.Mvc3.Attributes.CheckBoxListServerValidator]
        public IEnumerable<MH.Web.Mvc3.Models.CheckBox> LiquidAssetSourceList { get; set; }

 

In my Model constructor (isn't that where all lists should get instantiated?) I create the actual checkboxes

 

 

LiquidAssetSourceList = new List<MH.Web.Mvc3.Models.CheckBox>  
                {   new MH.Web.Mvc3.Models.CheckBox{ Name = "Bank" },
                    new MH.Web.Mvc3.Models.CheckBox{ Name = "Stocks" },

 

... and Here is what my View looks like:

 

@Html.LabelFor(model => model.LiquidAssetSourceList)
@Html.HiddenFor(m=>m.LiquidAssetClientValidator)
@Html.ValidationMessageFor(m=>m.LiquidAssetClientValidator)
@Html.CheckboxListFor(m=>m.LiquidAssetSourceList)
Here's the simple checkbox object:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace MH.Models
{
    public class CheckBox
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public bool IsChecked { get; set; } 
    }
}

 

And the HtmlExtension:

  public static System.Web.Mvc.MvcHtmlString CheckboxListFor<TModel, TProperty>(this HtmlHelper<TModel> html,
            Expression<Func<TModel, TProperty>> checkboxlist, object htmlattributes = null)
             where TProperty : IEnumerable<Models.CheckBox>
        {
            MemberExpression body = checkboxlist.Body as MemberExpression;
            string checkboxlistname = body.Member.Name;
 
            //Get currently selected values from the ViewData model
            IEnumerable<Models.CheckBox> cbs = checkboxlist.Compile().Invoke(html.ViewData.Model);
 
            StringBuilder sb = new StringBuilder();
            int idx = 0;
            foreach (Models.CheckBox cb in cbs)
            {
                string id = String.Format("{0}_{1}__IsChecked", checkboxlistname, idx);
                string name = String.Format("{0}[{1}].IsChecked", checkboxlistname, idx);
 
                TagBuilder itemdiv = new TagBuilder("div");
                itemdiv.AddCssClass("checkboxlist-itemdiv");
 
                TagBuilder label = new TagBuilder("label");
                label.AddCssClass("checkboxlist-label");
                label.MergeAttribute("for", id);
                label.InnerHtml = cb.Name;
                itemdiv.InnerHtml += label.ToString(TagRenderMode.Normal);
 
                TagBuilder inputcheck = new TagBuilder("input");
                if (cb.IsChecked) inputcheck.MergeAttribute("checked", "checked");
                inputcheck.AddCssClass("checkboxlist-checkbox");
                inputcheck.MergeAttribute("type", "checkbox");
                inputcheck.MergeAttribute("id", id);
                inputcheck.MergeAttribute("name", name);
                inputcheck.MergeAttribute("value", "true");
                itemdiv.InnerHtml += inputcheck.ToString(TagRenderMode.Normal);
 
                TagBuilder hiddencheck = new TagBuilder("input");
                hiddencheck.MergeAttribute("type", "hidden");
                hiddencheck.MergeAttribute("name", name);
                hiddencheck.MergeAttribute("value", "false");  // input checkboxes only post if checked, so we default to hidden with same id... mvc only takes first input of each id
                itemdiv.InnerHtml += hiddencheck.ToString(TagRenderMode.Normal);
 
                TagBuilder hiddenname = new TagBuilder("input");
                hiddenname.MergeAttribute("type", "hidden");
                hiddenname.MergeAttribute("name", String.Format("{0}[{1}].Name", checkboxlistname, idx));
                hiddenname.MergeAttribute("value", cb.Name);
                itemdiv.InnerHtml += hiddenname.ToString(TagRenderMode.Normal);
 
                TagBuilder hiddenid = new TagBuilder("input");
                hiddenid.MergeAttribute("type", "hidden");
                hiddenid.MergeAttribute("name", String.Format("{0}[{1}].ID", checkboxlistname, idx));
                hiddenid.MergeAttribute("value", cb.ID.ToString());
                itemdiv.InnerHtml += hiddenid.ToString(TagRenderMode.Normal);
 
                sb.Append(itemdiv.ToString(TagRenderMode.Normal));
                idx++;
            }
 
            TagBuilder div = new TagBuilder("div");
            div.AddCssClass("checkboxlist-paneldiv");
            div.MergeAttribute("id", checkboxlistname);
            div.MergeAttribute("name", checkboxlistname);
            div.MergeAttributes(new System.Web.Routing.RouteValueDictionary(htmlattributes));
            div.InnerHtml = sb.ToString();
 
            return new System.Web.Mvc.MvcHtmlString(div.ToString(TagRenderMode.Normal));
        } 

The attribute for the validator:

 

Namespace Attributes{
[AttributeUsage(AttributeTargets.Property)]  
    public class CheckBoxListServerValidator : ValidationAttribute 
    { 
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            if (value != null) 
            {  
                if ((value as IEnumerable<Euro.Web.Mvc3.Models.CheckBox>).Any(l => l.IsChecked)) return ValidationResult.Success;
            }
            return new ValidationResult("Please select an option.");
        }         
    }
    [AttributeUsage(AttributeTargets.Property)]  // validon bool? i dont think it matters
    public class CheckBoxListClientValidator : ValidationAttribute, IClientValidatable
    {
        public string ListName { get; set; }
        public CheckBoxListClientValidator(string listname)
        {
            ListName = listname;
        }
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        { 
            return ValidationResult.Success; 
        }
        public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {
            ModelClientValidationRule rule = new ModelClientValidationRule()
                {
                    ValidationType = "requiredcheckbox", // "requiredcheckbox" used in jquery
                    ErrorMessage = "Please select at least one"
                };
            rule.ValidationParameters.Add("listname", ListName);
            return new[] { rule };
        }
    }

and the js for the validator:

In custom-validators.js
/// (do not put validators inside document.ready)
/// these two are for MH.Attributes.CheckBoxListClientValidator 
$.validator.addMethod("requiredcheckbox",
                function (value, element, parameters) {
                    return $("#" + parameters.listname + " input:checked").length > 0;
                });
$.validator.unobtrusive.adapters.add("requiredcheckbox", ["listname"], function (options) {
    options.rules["requiredcheckbox"] = options.params;
    options.messages["requiredcheckbox"] = options.message;
}); 

Tags:

MVC

MVC

by MikeHogg 25. June 2012 10:06

 

In my second or third MVC3 project, I have really enjoyed Model Binding and my familiarity with the MVVM pattern in WPF translated well.  Web projects are much more enjoyable in c# for me than they were in the old webforms days.  Additionally, I found features in c# 4.0 that I really like, and frequently find myself taking advantage of nullable parameters, and lambda expressions.  I was asked to replicate legacy code in MVC3, which simply retrieved a feed of xml from a web service and turned them into a searchable data store of links.  Caching was a new feature of this.  The number of links was relatively small enough, that I decided to use file, not database, for the store, and simply creating my data model class with simple .net types allowed me to save my models in Properties.Settings.   In this code sample you will see the model, including a list of IEnumerable Headlines of either type of an enum, since we use this to serve up actually two different pages- InTheNews for public third party publications (WBAL, Women’s World) and NewsReleases (company generated PR)…

 

 

 

namespace SomeBase.Models
{
    public enum NewsType { InTheNews, NewsReleases };
    public class NewsModel
    {
        public string Title { get { return (NewsType == NewsType.InTheNews) ? "In The News" : "News Releases"; } set { var x = value; } }
        public NewsType NewsType { get; set; }
        public DateTime LastUpdateDate { get; set; }
        public IEnumerable<Headline> Headlines { get; set; }
        public string SearchString { get; set; }
        public string SearchType { get; set; }
        public List<System.Web.Mvc.SelectListItem> SearchTypes { get; set; }
        [DisplayFormat(DataFormatString = "{0:" + MH.Web.Mvc3.lib.CONST.DATEFORMATSTRING_SHORT + "}", ApplyFormatInEditMode = true)]
        public DateTime? SearchStartDate { get; set; }
        [DisplayFormat(DataFormatString = "{0:" + MH.Web.Mvc3.lib.CONST.DATEFORMATSTRING_SHORT + "}", ApplyFormatInEditMode = true)]
        public DateTime? SearchEndDate { get; set; }
         
    }
    public class Headline
    {
        [DisplayFormat(DataFormatString = "{0:" + MH.Web.Mvc3.lib.CONST.DATEFORMATSTRING_SHORT + "}")]
        public DateTime PostDate { get; set; }
        public HtmlString Link { get; set; }
        public NewsType NewsType { get; set; }
        public string ArticleType { get; set; }
    }
}

In the Controller, I use an abstract class because this might be served from the Desktop Web Site, or another project, for the Mobile site.  Most of the functionality is in the abstract controller, but the final  controller must implement the Save and Load functions of whatever data store they use, in our case the Properties.Settings.   Here I get to use the new Xml.Linq libraries which make loading an xml document from a url and parsing it one liners…

 

 

private Models.NewsModel GetFilteredNews(Models.NewsModel news)
        {
            Models.NewsModel allnews = GetAllNews(news.NewsType);
            news.SearchTypes = allnews.SearchTypes; // recreate dropdownlist- lost on postback?
            news.Headlines = allnews.Headlines
                .Where(h => h.NewsType == news.NewsType &&
                    (news.SearchEndDate == null || h.PostDate < news.SearchEndDate) &&
                    (news.SearchStartDate == null || h.PostDate > news.SearchStartDate) &&
                    (news.SearchType == null || news.SearchType == h.ArticleType) &&
                    (String.IsNullOrEmpty(news.SearchString) || h.Link.ToString().ToLower().Contains(news.SearchString.ToLower()))
                    ).OrderByDescending(h => h.PostDate);
            return news;
        }
        private Models.NewsModel GetAllNews(Models.NewsType newstype)
        {
            Models.NewsModel news = GetSavedNews();
            if (news.LastUpdateDate < DateTime.Now.AddDays(-1))
            {
                // get feed from vocus 
                Models.Headline[] latest = GetVocusFeed();
                if (latest.Count() > 0)  // if vocus is down?
                {
                    news.Headlines = latest;
                    news.LastUpdateDate = DateTime.Now;
                    SaveNews(news);
                }
            }
            // process for the view                
            news.NewsType = newstype;
            news.SearchTypes = news.Headlines.Where(h=>h.NewsType == newstype && !String.IsNullOrEmpty(h.ArticleType))
                .OrderBy(h => h.ArticleType).Distinct()
                .Select(h => new System.Web.Mvc.SelectListItem
                {
                    Text = h.ArticleType,
                    Value = h.ArticleType
                }).ToList();         
            return news;
        }
        private Models.Headline[] GetNewsFeed()
        {
            List<Models.Headline> headlines = new List<Models.Headline>();
            System.Xml.Linq.XDocument collateral = System.Xml.Linq.XDocument.Load(
http://somenewsfeed.com/Custom/CustomXmlFeed.aspx?something=something);
            System.Xml.Linq.XDocument news = System.Xml.Linq.XDocument.Load(
                http://somenewsfeed.com/Custom/CustomXMLFeed.aspx?somethingelse=something);
            headlines.AddRange(news.Descendants(System.Xml.Linq.XName.Get("NewsResults")).Select(e => new Models.Headline
            {
                ArticleType = GetField(e, "News_MediaOutletSortName"),
                Link = GetLink(e, "News"),
                NewsType = Models.NewsType.InTheNews,
                PostDate = DateTime.Parse(GetField(e, "News_NewsDate"))
            }).Where(e => e.Link != null));
            headlines.AddRange(collateral.Descendants(System.Xml.Linq.XName.Get("SomeResults")).Select(e => new Models.Headline
            {
                ArticleType = GetField(e, "A_Name"),
                Link = GetLink(e, "ALink"),
                NewsType = Models.NewsType.NewsReleases,
                PostDate = DateTime.Parse(GetField(e, "PublishDate"))
            }).Where(e => e.Link != null));
            return headlines.ToArray();
        }
 

 

 

The html page is simple enough, with clean model binding and the html helpers.  Note the handy “If” MvcHtmlString extension and the use of templating for Headlines. 

 

 

<h2 style="margin:30px;">@Html.DisplayFor(m=>m.Title)</h2>
@using (Html.BeginForm())
{
    
        @Html.ActionLink("Request Customized News", "RequestCustomizedNews").If(Model.NewsType == SomeBase.Models.NewsType.NewsReleases)
        <div style="float:left; width:200px; display:block;">
            <h3 >Search Archives</h3>
            <div class="display-label">Headline</div>
            <div class="display-field">
                @Html.EditorFor(model => model.SearchString)
            </div>
             
            <div class="display-label">Type</div>
            <div class="display-field">
                @Html.DropDownListFor(m=>m.SearchType, Model.SearchTypes, "All")
            </div>
            <div class="display-label">From:</div>
            <div class="display-field"> 
                @Html.TextBoxFor(model => model.SearchStartDate, new { @class = "calendar", @Value=Model.SearchStartDate.HasValue? Model.SearchStartDate.Value.ToString(MH.Web.Mvc3.lib.CONST.DATEFORMATSTRING_SHORT): null })
            </div>
            <div class="display-label">To:</div>
            <div class="display-field">
                @Html.TextBoxFor(model => model.SearchEndDate, new { @class = "calendar" })
            </div>
            <input type="submit" value="Go" />
        </div>
        <div style="float:left;">
            <h3>Recent Articles</h3>
            <div>
                @Html.DisplayFor(m => m.Headlines)
            </div> 
        </div>
    </div>
}
I am able to serve this one page “News” as two different Urls- InTheNews and NewsReleases, because of the enum, and MVC’s routing mechanism, which allows me to add a custom route, with a custom constraint.  (I have become a DRY fan, and both pages were nearly identical as it was, so I could hardly bear to repeat the code of two Views, two controller Gets, two controller Posts.)  I would make this more dynamic the next time I touched it by iterating through an enum parameter instead of passing hardcoded values but it was a neat exercise nonetheless…

 

 

 

{Global.asax}

            // Abouts
            routes.MapRoute(
                "About", // Route name
                "About/{newstype}", // URL with parameters
                new { controller = "About", action = "News" }, // Parameter defaults
                new { newstype = new OptionalConstraint(new string[]{"InTheNews", "NewsReleases"}) },
                new string[1] { "Some.Controllers" }
            );
public class OptionalConstraint : IRouteConstraint
    {
        string[] _mappedactions;
        public OptionalConstraint(string[] mappedactions)
        {
            _mappedactions = mappedactions;
        }
        public bool Match(System.Web.HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
        {
            if (routeDirection == RouteDirection.IncomingRequest)
            {
                return values[parameterName] == UrlParameter.Optional || _mappedactions.Contains(values[parameterName].ToString(), StringComparer.OrdinalIgnoreCase);
                
            }
            return false;
        }
    }

Tags:

MVC

Logging From Day One (and Exception Handling)

by MikeHogg 9. May 2012 09:50

NLog is so easy to use, it really is like plug and play. Or drag and drop. Add dll to your References. Add this to your web.config, use either file, or db table(what I use). Then, in any class you want to use Logger, just add a line for the static instance:

    public class HomeController : MH.Controllers.AController
    {
        private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); 

 

 

And then to use it:

 

    logger.Info("Some mess");

 

No reason not to have logging available in every web app from the start. I usually use a Log table described like my web.config shows here


<configuration>
  <configSections>
    <section name="nlog" type="NLog.Config.ConfigSectionHandler, NLog"/>...  </configSections>
...  <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" >
    <targets> 
      <target name="db" xsi:type="Database" connectionStringName="CONN"
              commandText="insert into Log(Level, Source, Message, Audit_Date) values(@level, @logger, @message, @time_stamp);">
        <parameter name="@time_stamp" layout="${date}"/>
        <parameter name="@level" layout="${level}"/>
        <parameter name="@logger" layout="${logger}"/>
        <parameter name="@message" layout="${message}"/>
      </target> 
    </targets>
 
    <rules>
      <logger name="*"  writeTo="db"></logger> 
    </rules>
  
  </nlog>

If you can't get it to start working, try using a log file first, or you can add atts like this example:
  <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        internalLogFile="c:\mike.log" internalLogToConsole="true" throwExceptions="true">
    <targets>
      <target xsi:type="File" name="file" fileName="${basedir}/n.log" />

Oh and while we're here, ELMAH is always in my projects even before NLog.  It's just as easy, and actually comes with more features.  I use it with teh DB Table, and automatic emails.  This is all you need to get up and running...

<configuration>
  <configSections>
    <sectionGroup name="elmah">
      <section name="security" requirePermission="false" type="Elmah.SecuritySectionHandler, Elmah" />
      <section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah" />
      <section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah" />
      <section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah" />
    </sectionGroup>
  </configSections>...
 
    <httpModules>
      <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" />
      <add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" />
      <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah" />
    </httpModules>
...  <system.webServer>
    <validation validateIntegratedModeConfiguration="false"/>
    <modules runAllManagedModulesForAllRequests="true"> 
        <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" preCondition="managedHandler" />
        <add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" preCondition="managedHandler" />
        <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah" preCondition="managedHandler" />
    </modules> 
  </system.webServer>... and 
  <elmah>
    <!--
        See http://code.google.com/p/elmah/wiki/SecuringErrorLogPages for 
        more information on remote access and securing ELMAH.   -->
    <security allowRemoteAccess="true" />
    <errorLog type="Elmah.SqlErrorLog, Elmah" connectionStringName="CONN"   >
    </errorLog>
    <errorMail
       to="mike.hogg@havasdiscovery.com"
       subject="[ELMAH] ACMT_Web Exception"  >
    </errorMail> 
    
  </elmah>
  <location path="elmah.axd" inheritInChildApplications="false">
    <system.web>
      <httpHandlers>
        <add verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" />
      </httpHandlers>
      <!-- 
        See http://code.google.com/p/elmah/wiki/SecuringErrorLogPages for 
        more information on using ASP.NET authorization securing ELMAH.      -->
      <authorization>
        <allow roles="Admin" />
        <deny users="*" />
      </authorization>
    </system.web>
    <system.webServer>
      <handlers>
        <add name="ELMAH" verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" preCondition="integratedMode" />
      </handlers>
    </system.webServer>
  </location>
</configuration> 

There's a db script to create the necessaries. I think that's it.  Comes with an Admin Area automatically and a dashboard app, if you set up authorization in your web then you should be able to see it with the Admin role and no further configuration.  ELMAH is good for catching all uncaught exceptions.  It has replaced my standard libraries and error handling methods in global.asax.

 

I also set up my own ErrorController, and some views, for my handled (known) errors.

public class ErrorController : AController
    {
        public ActionResult Index()
        { 
            Models.Error e = GetError();
            e.Title = "Error!";
            e.Message = "We are sorry.  An error has occurred.  Please try again or contact support";
 
            return View(e);
        }
 
        public ActionResult NotFound()
        {
            Models.Error e = GetError();
            e.Title = "Page Could Not Be Found";
            e.Message = "Sorry, that page could not be found";
 
            return View(e);
        }
 
        private Models.Error GetError()
        {
            Models.Error result = new Models.Error();
            Exception ex = null;
 
            try
            {
                ex = (Exception)HttpContext.Application[Request.UserHostAddress.ToString()];
            }
            catch { }
 
            if (ex != null) result.Exception = ex;
            
            return result;
        }

If you want to manually log errors in your app using ELMAH, just do this (wrapped in my lib/logger library):

 

 

public static void LogWebException(Exception ex)
        {
            try
            {
                Elmah.ErrorSignal.FromCurrentContext().Raise(ex, System.Web.HttpContext.Current);

 

Or... add a filter to Exception handling and in that hook tell ELMAH to log handled. Now all of your handled exceptions will be logged also.

namespace MH.Web.Mvc3.Controllers
{
    public class ElmahHandledErrorLoggerFilter : IExceptionFilter
    {
        public void OnException(ExceptionContext context)
        {
            // Log only handled exceptions, because all other will be caught by ELMAH anyway.
            if (context.ExceptionHandled)
                Elmah.ErrorSignal.FromCurrentContext().Raise(context.Exception);
        }
 
        // ADD THIS TO GLOBAL ASAX
        ///public static void RegisterGlobalFilters (GlobalFilterCollection filters)
        //{
        //    filters.Add(new ElmahHandledErrorLoggerFilter());
        //    filters.Add(new HandleErrorAttribute());
        //}
    }
}

 

 

 

ELMAH has a habit of becoming bothersome with all the 404s for robot.txt.   Put this in  your web.config to stop them..

 

 

    <errorFilter>
      <test>
        <or>
          <and>
            <equal binding="HttpStatusCode" value="404" type="Int32" />
            <equal binding="Context.Request.Path" value="/favicon.ico" type="string" />
          </and>
          <and>
            <equal binding="HttpStatusCode" value="404" type="Int32" />
            <equal binding="Context.Request.Path" value="/robots.txt" type="string" />
          </and>
        </or>
      </test>
    </errorFilter>
    
  </elmah>

About Mike Hogg

Mike Hogg is a c# developer in Brooklyn.

More Here

Favorite Books

This book had the most influence on my coding style. It drastically changed the way I write code and turned me on to test driven development even if I don't always use it. It made me write clearer, functional-style code using more principles such as DRY, encapsulation, single responsibility, and more. amazon.com

This book opened my eyes to a methodical and systematic approach to upgrading legacy codebases step by step. Incrementally transforming code blocks into testable code before making improvements. amazon.com

More Here