JQuery validation hooked up to HTML5 data attributes and some Bootstrap for a Salesforce page

by MikeHogg 9. October 2014 17:01

I was brought in as a front end developer on a Salesforce site to figure out a way to allow SF devs to add client side jQuery validation to dozens of fields as quickly and painlessly as possible.  Their application had only a few pages, but dozens of fields on each, and most of them required.  In addition, they had invisible widgets that were displayed and also required, based on answers of other fields.  So a page might be loaded with 10 fields, but when you check field number two, another panel with 3 fields shows up, and they are all required.

 

jQuery validation library is really easy to work with.  There are 7 or 8 standard validations that it knows already, like valid email addresses and required fields.  I just had to let the devs hook up with them easily, so I set up a scheme using new html5 data attributes.  I told them they just had to add

data-email=’must be a valid email address’

or

“data-required=’don’t forget!!’

to their html elements, and that was all.  I also gave them an easy way to have dependent validation, like when one field is required only if another field is checked.  And even another one for depending on a particular value being selected in dropdownlists, or any value being selected, that meant another field became required.

I came up with a quick script that hooked these data attributes up to the jQuery validator.  And used bootstrap style error messages.

var $jq = jQuery.noConflict();
 
/*
* customValidation.js
* Usage:
*      STANDARD BOOL VALIDATORS 
 *          add attribute data-{standard validation}="validation message"
*          to each html element that you want {standard validation like REQUIRED or EMAIL that are booleans}, including ones that show conditionally (like when you rerender a panel when clicking a radio)
*          {standard validation} is in jquery docs here http://jqueryvalidation.org/documentation/
*              includes required, email, url, date, dateISO, number, digits, creditcard
*          NOTE: if you are adding fields dynamically, from radio or checkbox selections, then their validations will be picked up automatically, on
*                  click, but if you are adding them with asynchronous ajax calls like with apex rerender actionsupport, 
 *                  then you need to add oncomplete="addValidation(this)" to your actionsupport tag
* 
 *      DEPENDS 
 *         add attribute  data-depends="htmlRenderedIdOfRadioOrCheckbox" data-val-message="validation message"
*         to each element that you want REQUIRED, based on whether RadioOrCheckbox is checked.  
 *         
 *      DEPENDS-DROPDOWN
*          same as DEPENDS, but also uses data-parm="{DropdownValue}" to test against.  
 * 
 *      DEPENDS-DROPDOWN-ANY
*          same as depends-dropdown, but no data-parm b/c it depends on anything being selected
*/
$jq().ready(function () {
    
    setValidation();
 
    $jq('input[type=radio],input[type=checkbox]').on('change', function () { addValidation(this) });
    
});
 
function setValidation(){
    var myRules = {}, myMessages = {};
     
    function extendValidator(validatorType) {
        var valType = validatorType.toString();
        var att = "[data-" + valType + "]";
 
        $jq(att).each(function (i) {
            var myBool = {};
            myBool[valType] = true;
            var myRule = {};
            myRule[this.id] = myBool;
            $jq.extend(true, myRules, myRule);
 
            var myText = {};
            myText[valType] = $jq(this).data(valType);
           var myMessage = {};
            myMessage[this.id] = myText;
            $jq.extend(true, myMessages, myMessage);
        });
 
    }
 
    $jq.each(["required", "email", "url", "date", "dateISO", "number", "digits", "creditcard"], function () { extendValidator(this); } );
 
    $jq("[data-depends]").each(function (i) {
        myRules[this.id] = {
            required: {
                depends: function (ele) {
                    return document.getElementById($jq(this).data("depends")).checked;
                }
            }
        };
        myMessages[this.id] = {
            required: $jq(this).data("val-message")
        };
    });
 
    $jq("[data-depends-dropdown]").each(function (i) {
        myRules[this.id] = {
            required: {
                depends: function (ele) {
                    return $jq(document.getElementById($jq(this).data("depends-dropdown"))).val() == $jq(this).data("parm");
                }
            }
        };
        myMessages[this.id] = {
            required: $jq(this).data("val-message")
        };
    });
 
    $jq("[data-depends-dropdown-any]").each(function (i) {
        myRules[this.id] = {
            required: {
                depends: function (ele) {
                    return $jq(document.getElementById($jq(this).data("depends-dropdown-any"))).prop('selectedIndex') > 0;
                }
            }
        };
        myMessages[this.id] = {
            required: $jq(this).data("val-message")
        };
    });
 
 
 
    // validate() initializer
    $jq("form").validate({
                                                                        debug:true,
        rules: myRules,
        messages: myMessages,
 
        // for accordion
        ignore: [],  
        invalidHandler: function (event, validator, element) { 
            if (validator.numberOfInvalids() > 0) {
                validator.showErrors();
                $jq(".has-error").closest(".panel-collapse:not(.in)").collapse('show');
                $jq(".has-error").closest(".panel-collapse:not(.in)").on('shown.bs.collapse', function () {
                    var arbitraryElement = $jq(".has-error").find("input")[0];
                    arbitraryElement.focus();
                    arbitraryElement.scrollIntoView(true);
                });
            }
        },
 
 
        // bootstrap 3.0 styles- you need to use bootstrap classes like class=form-group in your divs 
        errorElement: "span",
        errorClass: "help-block",
        highlight: function(element) {
            $jq(element).closest('.form-group').removeClass('has-success').addClass('has-error');
        },
        unhighlight: function(element) {
            $jq(element).closest('.form-group').removeClass('has-error').addClass('has-success');
            $jq(element).popover("hide");  // should be called in showErrors but when removing rules via Depends, showErrors doesn't fire
        },
        errorPlacement: function (error, element) {
            if (element.parent('.input-group').length || element.prop('type') === 'checkbox' || element.prop('type') === 'radio') {
                error.insertAfter(element.parent());
            } else {
                error.insertAfter(element);
            }
        },
 
        // bootstrap popovers, needs boostrap.js might conflict with jquery.ui.js
        showErrors: function (errorMap, errorList) {
 
            $jq.each(this.successList, function (index, value) {
                $jq(value).popover('hide');
            });
 
            $jq.each(errorList, function (index, value) {
                var _popover;
                _popover = $jq(value.element).popover({
                    trigger: "manual",
                    placement: "top",
                    content: value.message,
                    template: "<div class=\"popover\"><div class=\"arrow\"></div><div class=\"popover-inner\"><div class=\"popover-content\" style=\"font-weight:normal\"><p></p></div></div></div>"
                });
                _popover.data("bs.popover").options.content = value.message;
                $jq(value.element).popover("show");
            });
 
            this.defaultShowErrors();
        }
 
    });
}
 
/* 
 * used for creating rules after Validate()
*/
function addValidation(ele) {  
 
    $jq.each(["required", "email", "url", "date", "dateISO", "number", "digits", "creditcards"], 
        function(){
            var attType = this.toString();
            var attSelector = "[data-" + attType + "]";
            $jq(attSelector).each(function() {
                if (attType in $jq(this).rules() === false){
                    var myRule = {};
                    myRule[attType] = true;
 
                    var myMessage = {};
                    var msg = this.getAttribute("data-" + attType);
                    myMessage[attType] = msg;
                    myRule.messages = myMessage;
                    $jq(this).rules("add", myRule);
                }
            });
    });
     
}
 
 
function addMike(e) {
   if ($jq("#mike").length == 0) {  
        $jq("#lblFLName").append("<input type='text' id='mike' name='mike' value='' data-required='yesyesyes' placeholder='someplaceholder mike' ></input>");
    }
    else $jq("#mike").remove();
}

Tags:

JQuery

Refactoring MVP and custom javascript date controls

by MikeHogg 22. September 2014 17:11

I was tasked to fix a custom UI date control in a legacy .Net web application. It was a requirement that each of the date components- day, month, year- had to be in separate dropdownlists. I recommended going with one of the standard date controls that exist in any of the .net or js libraries today (and have been extensively tested - hundred of hours in some cases) but there was a reason for going with a custom control. It was originally written years ago, and I ended up entirely rewriting it because of some design conflicts. 

This was a legacy maintenance project, where only emergency fixes are allowed, and no upgrades or refactoring time is available. Like most of these projects, I wanted to keep as close to the existing tools/libraries/patterns as I could, and write as little as possible custom code; I wanted the fewest changes.  So...

The site was using the old Model View Presenter Pattern (pre MVC), and there were runtime properties of the control, that were not available to the controller during design time.  My controller was going to need these properties, so I followed the existing pattern and set up an interface in the library and exposed my control properties to the controller through this library interface, so we could process these design time variables in our controller and wouldn’t get any surprise runtime exceptions.

The c# code was keeping track of each of the dropdownlists manually, so there were Day Month Year properties to select and clear, and the lists had to be populated depending on currently selected values, and selected values had to be selected or cleared at the right parts of the page lifecycle if we were posting back, and at other parts if we weren't, and gotten from Request values when they weren't available yet in the page lifecycle.  It was all very complicated and manual. I replaced all that with databinding for populating the lists, and just two properties- one SelectedDate property (get only) and one PreselectedDate property(set only) that were databound as well.

It was also using a lot of javascript, and a lot of it was the same as the c# code, populating the lists in the dropdowns again, but this time in the client, based on whether there were 30 or 31 or 29 days in that month and so on when the user changed them... The problem with this was the list counts of the control that was posted back would not match the list counts of the control in viewstate, and .net would barf on

“control tree not matching viewstate(Invalid Postback or callback argument. EventValidation is enabled using enableEventValidation="true") “

and you don't want to turn EventValidation off, or what's the whole point of using .net and viewstate?  Instead, what I do here, is add an AjaxPanel, or a Telerik ajax panel, since Telerik is what they were using in this case, and then all I had to do was call postback onchange of the dropdownlists, and use the same c# databinding code I already wrote.  Removed all the duplicate js code and no more EventValidation exceptions.

Above this, I was supposed to add validation.  (client side only – those were the requirements)  Which is the cool part that I'd like to share.  I could have done this a couple different ways but ended up writing my own short validation function in lieu of some other existing options that would have added much more (still custom) complexity.

Javascript dates are notoriously difficult to work with.  They have carryover parsing, and the zero index months.  Javascript can take a date(2011,12,1) and parse it to be 2012/1/1.  But if I just compare my js date fields after parsing, to my input fields, I could make sure the date is not only valid, but the expected date entered.

 

 

function validateDddp() {
            $('.txtDobValid:first').val('');
 
            var day = $('.dateList')[0].value;  // using asp controls so Id can't be used, using class instead
            var month = $('.dateList')[1].selectedIndex -1;  //js: zero based months
            var year = $('.dateList')[2].value;
            
            var adate = new Date(year, month, day);
            if (isValidDateDddp(adate) 
                && adate.getDate() == day 
                && adate.getMonth() == month
                && adate.getYear() == year - 1900) {
                    $('.txtDobValid:first').val('valid');
            }
 
            $('.txtDobValid:first').change();
        }
 
        function isValidDateDddp(d) {
            if (Object.prototype.toString.call(d) !== "[object Date]")
                return false;
            return !isNaN(d.getTime());
        }
 
        // for page loads where field is already populated 
        $(document).ready(function() {
        if ($.grep($('.dateList'), function(e) { return e.selectedIndex != 0; }).length > 0) {
                validateDddp();
            }
        }
        );

 

For kicks here is the web View custom user control code-behind that handles all the binding/page load scenarios, and now also the ajax posting. Pretty basic.

[System.Web.UI.ValidationProperty("SelectedDate")]
public partial class DropDownDatePicker : Microsoft.Practices.CompositeWeb.Web.UI.UserControl, IDropDownDatePickerWebPartView
{
    private DropDownDatePickerWebPartPresenter _presenter;
    private DateTime dateOfBirth;
     
    public DateTime? SelectedDate
    {
        get
        {
            DateTime result;
            if (DateTime.TryParse(String.Format("{0}/{1}/{2}", this.ddlYear.SelectedValue, this.ddlMonth.SelectedIndex, this.ddlDay.SelectedValue), out result)) return result;
            else return null;
        }
    }
    public DateTime DateOfBirth { set { dateOfBirth = value; } } 
    public DateTime MinDate { get; set; }
    public DateTime MaxDate { get; set; }
    public bool ValidationEnabled { get; set; }
 
 
    [Dependency]
    public DropDownDatePickerWebPartPresenter Presenter
    {
        get
        {
            return this._presenter;
        }
        set
        {
            if (value == null)
                throw new ArgumentNullException("value");
 
            this._presenter = value;
            this._presenter.View = this;
        }
    }
 
 
    protected void Page_Load(object sender, EventArgs e)
    {
        if (MaxDate == DateTime.MinValue)
        {
            MinDate = DateTime.Now.AddYears(-100);
            MaxDate = DateTime.Now;
        }
 
        if (!this.IsPostBack)
        {
            this._presenter.OnViewInitialized();
 
            rfvDateOfBirth.Enabled = ValidationEnabled;
 
            BindLists();
 
            BindSelections();
        }
                   
        this._presenter.OnViewLoaded();
 
    }
 
 
    #region Binds
    private void BindLists()
    {
        ddlDay.DataSource = GetDays(dateOfBirth.Year, dateOfBirth.Month);
        ddlDay.DataBind();
 
        ddlMonth.DataSource = GetMonths();
        ddlMonth.DataBind();
 
        ddlYear.DataSource = GetYears();
        ddlYear.DataBind();
    }
 
    private void RebindDays()
    {
        int year;
        var month = ddlMonth.SelectedIndex;
        var currentday = ddlDay.SelectedValue;
 
        if (ddlYear.SelectedIndex == 0) year = DateTime.Now.Year;
        else int.TryParse(ddlYear.SelectedValue, out year);
 
        if (year <= MaxDate.Year
            && year >= MinDate.Year
            && month > 0
            && month <= 12)
        {
            ddlDay.DataSource = GetDays(year, month);
            ddlDay.DataBind();
            if (ddlDay.Items.FindByValue(currentday) != null) ddlDay.SelectedValue = currentday;
        }
        
    }
    
    private void BindSelections()
    {
        if (dateOfBirth != DateTime.MinValue)
        {
            this.ddlYear.SelectedValue = dateOfBirth.Year.ToString();
            this.ddlMonth.SelectedIndex = dateOfBirth.Month;
            this.ddlDay.SelectedValue = dateOfBirth.Day.ToString();
        }
    }
 
    #endregion
 
 
    #region Get Lists
    private List<string> GetDays(int year, int month)
    {
        var days = new List<string>();
        days.Add("-Day-"); 
        while (days.Count <= DateTime.DaysInMonth(year, month)) days.Add(days.Count.ToString());
        return days;
    }
 
    /// <summary>
    /// We'll can still use Months as List<string> if we use Index for the Int when creating DateTime objects
    /// </summary>
    /// <returns></returns>
    private List<string> GetMonths()
    {
        var months = new List<string>();
        months.Add("-Month-");
        while (months.Count <= 12) months.Add(new DateTime(2001, months.Count, 1).ToString("MMMM"));
        return months;
    }
 
    private List<string> GetYears()
    {
        var years = new List<string>();
        years.Add("-Year-");
        int thisyear = MaxDate.Year;
        int floor = MinDate.Year;
        while (thisyear > floor) years.Add(thisyear--.ToString());
        return years;
    }
 
    #endregion
 
 
    #region Events
    protected void ddl_SelectedIndexChanged(object sender, EventArgs e)
    {
        RebindDays();
        Validate();
 
    }
 
    #endregion
 
    #region Validation
 
    private void Validate()
    {
        if (ValidationEnabled)
        {
            try
            {
                int year = Convert.ToInt16(this.ddlYear.SelectedValue);
                int month = Convert.ToInt16(this.ddlMonth.SelectedIndex);
                int day = Convert.ToInt16(this.ddlDay.SelectedValue);
 
                DateTime d = new DateTime(year, month, day);
                if (d > MaxDate || d < MinDate)
                {
                    throw new Exception(String.Format("Server Side DropDownDatePicker.Validate: Date {0} not in valid range {1} - {2}",
                        d.ToShortDateString(), MinDate.ToShortDateString(), MaxDate.ToShortDateString())); 
                }
            }
            catch (Exception e)
            {
                //log... 
                txtDobValid.Text = String.Empty;
                rfvDateOfBirth.IsValid = false;
                // leave parent to validate page
            }
        }
    }
 
    #endregion
 
 
}
}

Quartz Scheduler

by MikeHogg 26. October 2013 13:46


Quartz Scheduler seems to be a very robust and mature job scheduler originally created in java.  I’ve only just found out about it and find it useful enough to make a note here about it for future use.  You can find lots of tutorials and samples and users online for it.  It is ported to .Net with Quartz.net.  Adding a quartz scheduler to your project is as easy as a Nuget.  Simple scheduling can happen right out of the box.  It can handle complicated cases, and just about all of the scheduling cases that I've seen.  JobDetails, Jobs, and Triggers are all classes you can mix and match up in different combinations.  It also handles persistence if you want to hook up to an ado store (several Sql and NoSql avail), and logging is built in with another Nuget- Common.Logging.

 

If you want to inject services into your IJobs, though, you will need to create your own IJobFactory and start the scheduler that way

Dim schFactory As ISchedulerFactory = _container.Resolve(Of ISchedulerFactory)()
_scheduler = schFactory.GetScheduler()
_scheduler.JobFactory = New ScheduleJobFactory(_container)

Your factory implements NewJob (and with Quartz 2.2, ReturnJob (do nothing unless you have dispose requirements)) and here is where you craete the job with your Container of choice (Autofac here) so the container can inject what it needs...

public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
        {
            var jobtype = bundle.JobDetail.JobType;
            try
            {
                var schjobtype = typeof(MyJob<>).MakeGenericType(jobtype);
                var schjob = (IJob)Activator.CreateInstance(schjobtype, _container);
                return schjob;
            }
            catch (Exception e)
            {
                using (var l = _container.BeginLifetimeScope())
                {
                    var logger = _container.Resolve<ILogger>();
                    logger.Error(e);
                }
            }
            return new NoOpJob();
        }

... Your JobFactory gets the container from it's constructor

public class ScheduleJobFactory : ISchedulerFactory
    {
        ILifetimeScope _container;
        public ScheduleJobFactory(ILifetimeScope container)
        {
            _container = container;
        } 
    }

I couldn't have figured this part out:  Your jobfactory points to a jobtype class of T that does the container resolve of T (!) clever pattern.

public class MyJob<T> : IJob where T : IJob
    {
        ILifetimeScope _container;
        public MyJob(ILifetimeScope container)
        {
            _container = container;
        }
        public void Execute(IJobExecutionContext context)
        {
            using (var lscope = _container.BeginLifetimeScope())
            {
                var someJob = lscope.Resolve<T>();
                someJob.Execute(context);
            }
        }

 

Your Jobs still inherit from IJob and don't reference the generic job... just register them like normal, and specify these specific classes, in your quartz.jobs.xml

 

...

 

Diagnostics- if you have trouble getting your job xml files to work (or are trying to use deprecated 1.0 version xml with upgraded 2.0 quartz) and want to see the logging, you can add Common.Logging config to your app.config.  The section def:

 

<sectionGroup name="common">
      <section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging" />
</sectionGroup>

The config-

  <common>
    <logging>
      <factoryAdapter type="Common.Logging.Simple.ConsoleOutLoggerFactoryAdapter, Common.Logging">
        <arg key="level" value="DEBUG" />
        <arg key="showLogName" value="true" />
        <arg key="showDataTime" value="true" />
        <arg key="dateTimeFormat" value="yyyy/MM/dd HH:mm:ss:fff" />
      </factoryAdapter>
    </logging>
  </common>

 

 

I've seen lots of online users hook up their common.logging to log4net to monitor the scheduler process, and there is a FactoryAdapter for log4net if you want to go that route.

 

While we are in config, note that the quartz looks for config in 3 or 4 places.  First in a java style file quartz.config, then in app.config, then in some other places.  If you want to remove the quartz.config and use just .net style, this is a sample app.config section def-

<section name="quartz" type="System.Configuration.NameValueSectionHandler, System, Version=1.0.5000.0,Culture=neutral, PublicKeyToken=b77a5c561934e089" />

 

and config-

  <quartz>
    <add key="quartz.scheduler.instanceName" value="_scheduler" /> <!-- whatfor-->
    <!--     Configure Thread Pool -->
    <add key="quartz.threadPool.type" value="Quartz.Simpl.SimpleThreadPool, Quartz" />
    <add key="quartz.threadPool.threadCount" value="10" />
    <add key="quartz.threadPool.threadPriority" value="Normal" />
    <!--     Configure Job Store --><!--
    <add key="quartz.jobStore.type" value="Quartz.Simpl.RAMJobStore, Quartz" />
      <add key="quartz.plugin.xml.type" value="Quartz.Plugin.Xml.JobInitializationPlugin, Quartz" />-->
    <add key="quartz.plugin.xml.fileNames" value="~/config/ScheduledJobs.config" />
    <!--<add key="quartz.plugin.xml.scanInterval" value="10" />-->
    <add key="quartz.plugin.xml.type" value="Quartz.Plugin.Xml.XMLSchedulingDataProcessorPlugin, Quartz" />
  </quartz>

That uses the defaults.  The XmlSchedulingDataProcessorPlugin I think is the included jobs.xml reader and required if you use xml, as the default it something else.

Tags:

Architecture | C# | VB.Net

NHibernate tricks

by MikeHogg 22. April 2013 19:51

 

When setting up Automapper you usually use a TableNameConvention that takes plurals, but if you are like me and working against an existing database, you can opt not to use it and save yourself the time of all those mapping.Table("Tablename") redundancies in your override maps.

 

Sharp Architecture and Templify is a great approach to internal or in-house starter library, although it is quick to branch and already with MVC4 out and MVCContrib deprecated, or simply out of date, I get the feeling that the main Sharp Architecture team project does not look like my version anymore.

One of the main things to do when setting it up out of the box with MVC4 is redirecting the old strong named dll references to the new ones.  It took four of them for me.  This appeared to be caused mostly by MVCContrib's strong typed controller actions, which is one of my favorite features.  I cannot find anything about this library being updated for MVC4 so I hesitate to put it in a new production project.  Also, I was disappointed to see magic string ActionLinks still in MVC4.  Anyway, I had to add this to my web.config...

 

 
 
  <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="System.Web.WebPages.Razor" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="0.0.0.0-2.0.0.0" newVersion="2.0.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.Helpers" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0-2.0.0.0" newVersion="2.0.0.0" />
      </dependentAssembly>
      	        <dependentAssembly>
        	            <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" />
                <bindingRedirect oldVersion="1.0.0.0-4.0.0.0" newVersion="4.0.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.WebPages" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="0.0.0.0-2.0.0.0" newVersion="2.0.0.0" />
      </dependentAssembly>
    </assemblyBinding>

Tags:

NHibernate

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

NHibernate midstream

by MikeHogg 7. March 2013 14:30

NHibernate is an active ORM product, and has been one for some time.   Coming to it as I have, at version 3 can be difficult to digest, especially as you are soaking up new syntax, even if the patterns are not unfamiliar.  Especially since there are many options and support libraries to choose from, and the core library has changed so much from version one to two and again to three. 

So just blindly googling lines of code and quickstarts can easily get you in lots of dark woods unless you pay attention to the dates of web posts, and understand how NH evolved over time. 

First, the patterns.  NH works from a Repository pattern by using db sessions.  Inject the NH SessionFactory into your repositories and then your Repos can run all the NH Query goodness instead of you writing a whole bunch of db layer stuff.  In its simplest form, you can use an NHHelper class to statically create a SessionFactory inside your Repo, or you can use Construction Injection and let your Injection Library do that for you automatically, like here with Ninject

public class NHibernateSessionFactory
    {
        public ISessionFactory GetSessionFactory()
        { //later }
        public class NHibernateSessionFactoryProvider : Ninject.Activation.Provider<ISessionFactory>
        {
            protected override ISessionFactory CreateInstance(Ninject.Activation.IContext context)
            {
                var sessionFactory = new NHibernateSessionFactory();
                return sessionFactory.GetSessionFactory();
            }
        }

 

then your Repo looks something like this:

 

 public class Repository<T> :  IRepository<T> where T:class
    {
        private readonly ISession session;
 
        public Repository(ISession s)
        {
            session = s;
        }
 

So GetSessionFactory is where all the NH code is config'd.

Here are your options for configuring...

Originally, everything was XML and config based, so you would have the base NH.config() in your code, and your web.config would have a section with the required and optional properties in it.

 

 
public ISessionFactory GetSessionFactory()
        {
            var config = new Configuration();
            config.BuildSessionFactory();
 

 

Then your web.config would have all the hookup:

 

 <configSections>
    <section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectio
nHandler, NHibernate" />
</configSections>
  <connectionStrings>
    <add name="CONN" connectionString="blahblah" providerName="System.Data.SqlClient" />
  </connectionStrings>
  <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
    <session-factory>
      <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
      <property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
      <property name="dialect">NHibernate.Dialect.MsSql2008Dialect</property>
      <property name="connection.connection_string_name">CONN</property>
      <property name="show_sql">true</property>
    </session-factory>
  </hibernate-configuration>
 

Then came Fluent, and config was moved into code. so, if you use that library, you no longer need the web.config and you can just use this in your SessionFactory...

 

        
public ISessionFactory GetSessionFactory()
        {
            // fluent style config.  We use both to apply loquacious mapping by code to old style config, and then create FluentConfiguration with it
            FluentNHibernate.Cfg.FluentConfiguration fluentConfiguration = FluentNHibernate.Cfg.Fluently.Configure()
                                                   .Database(FluentNHibernate.Cfg.Db.MsSqlConfiguration.MsSql2008.ShowSql()
                                                    .ConnectionString(c => c.FromConnectionStringWithKey("CONN")))
                                                    // only need one From Assembly to point to all the maps These are for Fluent MAPS inherting from ClassMap
                                                   .Mappings(m => m.FluentMappings.AddFromAssemblyOf<Maps.EventMap>())                
                                                   .ExposeConfiguration(cfg => cfg.SetProperty("adonet.batch_size", "20"))
                                                   .ExposeConfiguration(c => c.SetProperty("generate_statistics", "true"));                                       
                                                    //.ExposeConfiguration(BuildSchema) 
 
            return fluentConfiguration.BuildSessionFactory();

In addition to config, when setting up your NH Environment, you need to init your maps.  If you already have database tables, the first order of business is to get NMG, NHibernateMappingGenerator.  This reads the db and outputs entity classfiles and classmaps and is a huge help.  Note in the screenshot below, in the upper right hand side of the preferences, how you can choose which format of Mapping files you want.

 

Originally, these would be XML files, (convention) named something.hbm.xml.  We no longer need to do that.  You see also FluentNHibernate and the new Loquacious format.  FNH uses ClassMap parent class, and I believe that might be the convention that allows AddAssemblyFromMaps<>() to work.  Loquacious uses ClassMapping parent, and I believe offers more flexibility in filtering types from Assembly to map.

With NH 3 the project merged the Loquacious (Mapping By Code) codebase that was previously a library, but it is not very mature so a lot of posts I have seen are where it is being used for simpler project requirements or in tandem with a large base of existing Fluent mappings.  I could not figure out a way to add MBC maps to FluentMappings, so in case you ever want to do that here is code to initialize and map both ways, using a base NH.Config with web.config for MBC maps and building your FluentConfig with that base, adding FluentMaps in that process...

 
public ISessionFactory GetSessionFactory()
        {
            
            // old style config 
            var mm = new NHibernate.Mapping.ByCode.ModelMapper();
            Type[] mappingTypes = typeof(Maps.EventMap).Assembly.GetExportedTypes().Where(t => t.Name.EndsWith("Map")).ToArray();
            mm.AddMappings(mappingTypes);
 
            var config = new Configuration(); 
            config.AddMapping(mm.CompileMappingForAllExplicitlyAddedEntities());
 
 
            // fluent style config.  We use both to apply loquacious mapping by code to old style config, and then create FluentConfiguration with it
            FluentNHibernate.Cfg.FluentConfiguration fluentConfiguration = FluentNHibernate.Cfg.Fluently.Configure(config)
                                                   .Database(FluentNHibernate.Cfg.Db.MsSqlConfiguration.MsSql2008.ShowSql()
                                                    .ConnectionString(c => c.FromConnectionStringWithKey("CONN")))
                                                    // only need one From Assembly to point to all the maps These are for Fluent MAPS inherting from ClassMap
                                                   .Mappings(m => m.FluentMappings.AddFromAssemblyOf<Maps.EventMap>())                
                                                   .ExposeConfiguration(cfg => cfg.SetProperty("adonet.batch_size", "20"))
                                                   .ExposeConfiguration(c => c.SetProperty("generate_statistics", "true"));                                       
                                                    //.ExposeConfiguration(BuildSchema) 
 
            return fluentConfiguration.BuildSessionFactory();
        }

I like crosswords

by MikeHogg 1. January 2013 21:13

I like puzzles.  I didn't know that I would like my job when I started, but I consider myself lucky that I really enjoy what I do.

Last year I was working a team at a local commodities trading company that had fallen into mostly production support.  They had several new development projects that they wanted me to do, but they also inherited a large number of old legacy processes that were starting to break, about 1000 of them.  They just didn’t have enough staff to handle both and of course, fixing legacy apps always comes first.

I went to work for a small marketing agency.  They mostly did print and media ads, but had a small development team for their growing digital marketing campaigns.  They asked me to bring them up to speed on MVC and so I started by learning MVC myself and delivering a medium sized web in my first few months there.   While I was there I delivered a few medium web apps, and a few small web apps, as well as an in-house wiki (Foswiki/Perl) for knowledge sharing and automated deployments (Jenkins/java).

I fell in love with MVC, and was able to get up to speed quickly based on my previous two years working with the MVVM pattern in WPF.  I actually found MVC Model binding simpler that WPF and page actions easier to use than WPF Commands. 

I designed one of the medium sized and all of the small web apps from beginning to end, from requirements gathering and database design through to testing and maintenance.  The largest of them had three phases of delivery over nine months using prototypes and waterfall design methodology. 

Before that I had five years of project work in the commodities group of that local company.  I was a contractor and worked on four different teams while I was there, getting my six month contract extended for the entire five years.  One team was of a dozen developers, and others with only a few. 

The first team I was with was the largest one.  We had over  a dozen devs at one time, and a large enterprise web application and database application to support and enhance.  I was tasked with delivering an issue tracking system of any sort that would be better than emails and spreadsheets.  After some research on all kinds of options from writing my own to implementing and customizing an existing free app, I settled on Eventum, which was a mature, open sourced PHP webapp written by the MySql team.  This went over extremely well, and I extended and customized it over the next couple of years as it expanded to several other teams besides ours.

In addition to that, and writing the legacy enhancements and bugfixes for my two years on this team, I had another cool project that I wrote mostly on the side.  We had TV screens on the walls, and my boss wanted to see metrics of all of our systems on one of them.  I used a single .net web page and used some Dundas charts and gauges for the front end, but the back end was a bunch of multi threaded calls to over twenty different data stores, each with different caching schemes and data interfaces.

My last couple of years there were spent doing double duty leading two small teams of two to four developers.  I rewrote a medium sized enterprise database application of 20k lines and delivered a WPF application to move all maintenance from IT to the business users.  Basically, I let them update their own data, with authorization, and auditing.  Updating their own data had been a complicated, manual, multi step process that took a few days  and required a coordinated effort with a business user and an IT resource.  I automated this process, and gave the business users the tool to do it themselves.  Although the database wasn’t a rewrite from the start, after two years of constant refactoring I had trimmed the original stored procedure codebase in size to a third of what it was and converted a mess of duplicated spaghetti code to an extensible, documented, and easily maintained set of procedures.

The other team I led supported the process that extracted data for a vendor data warehouse.   It was a small data store, in the realm of 250k rows, but it involved again a series of manual, complicated steps for an IT resource to follow perfectly every time it was requested by the business user.  I automated the extraction process, and delivered again a WPF application for the user to control the extraction, with logging, and even history, so that they could go back to previous extractions.

 

 

 

Tags:

Me

Dependency Injection is not a bad word

by MikeHogg 12. December 2012 10:10

"Dependency Injection" is not a bad word.  Besides the fact that it's two words, if anything is bad abou it, it is only that "Dependency" is a bad word,  not Injection.  If you can't encapsulate everything necessary for your class or system to work, then what you want to do is expose your dependencies, in a contract, so other developers can easily figure out what is needed to get what they want from your library.

 

You can do this by limiting constructor signatures, or method signatures, to individual properties that you rely on, or by providing a signature that takes an entire class, and that is what they usually call dependency injection.

 

Let's say you have a custom MembershipProvider, and it provides several public methods, which other developers might find browsing your namespace.  Let's say another developer finds what she is looking for in your dll, and so tries to use it in his case. Let's say you aren't exposing any static methods, so they must now instantiate this object, and any dependencies your classes have become an issue.

Tags:

Architecture

Roll Your Own Sorting in 2012

by MikeHogg 17. November 2012 09:51

Had a client request that turned into rolling my own filter/paging/sorting.  I've done this so many times before, but not much in the last five years, as it has become a more or less 'built in' feature in most framework user controls.  I have never kept my own library of code for this, because in each case the specific use was customized and the result was quite spaghetti like and nothing I was proud of.

But this time, I think I generalized it enough, and plus, I was able to use a predicate pattern I've used before and so I wanted to write this up for future reference.

 

Oh, the client codebase is in VB.  My first time using VB since college.  I'd prefer not to use it again either, but at least the .net libraries are the same.

 

Remember from previous efforts the state issues, so careful attention to what steps we do at each specific step throughout page lifecycle...

 

We're using aspnet ListView control, with templates and the aspnet PagerControl, which has an eccentricity where you have to databind after Page_Load, usually in Page_PreRender, so we GetData in PageLoad and save it in a page property without binding yet.  Also in Page_Load we bind all of our filter list controls.

 

Here's the Page_Load's GetData. Notice we actually databind a ListView here but that one is the hidden one.  We use a hidden one that is unpaged for the google map pushpins.

 

 

Public Sub GetData()
 
            MyCarFeed = New CarFeed(Me.objDP.GetListItems(Me.ModuleID, Me.objCore.Language.CurrentLanguage, "sort").Tables("cars"))
 
            Dim filtertext As String = Me.txtFilter.Text.ToLower()
 
            Filtered = MyCarFeed.Cars.Where(Function(c) _
                (String.IsNullOrEmpty(filtertext) OrElse String.Concat( _
                 c.Description, _
                 c.Subtitle, _
                 c.Requirements, _
                 c.CompanyInformation, _
                 c.ContactInfo, _
                 c.Name).IndexOf(filtertext) > -1)). _
            Where(ColorFilter()). _
            Where(SpecialFilter()). _
            Where(AgeFilter()). _
            Where(SizeFilter()). _
            OrderByDescending(Function(c) c.DateFrom)
 
            ' while we are here
            Me.LvHiddenForMap.DataSource = Filtered
            Me.LvHiddenForMap.DataBind()
 
        End Sub

 

 

in the Page_PreRender we call BindPagedList... As long as I can bind again in prerender the PageControl takes care of paging automatically.

 

 

 
Private Sub BindPagedList()
            If _sorted = False Then
                Sort(ViewState("SortExpression"))
            End If
 
            Me.lv.DataSource = If(Filtered Is Nothing, Filtered, Filtered.ToArray())
            Me.lv.DataBind()
 
        End Sub
 

 

 

Oh another detail for the finicky DataPager Control, in our case, because we are using it in a template, so we can't add an event to it in markup, we call this method on init:

Protected Sub SetPageSize(ByVal sender As Object, ByVal e As System.EventArgs) ' handles DataPager Init... but it's a templated control so we have to hook it up in html event 
            If (Not Me.CurrentCarsPageSize Is Nothing) Then
                CType(sender, DataPager).PageSize = Me.CurrentCarsPageSize
            End If
        End Sub

 

 

So if we are posting back on a sort then we call Sort here.  It's the same Sub we call on the Sorting event:

 

 

 
Protected Sub Sorting(ByVal sender As Object, ByVal e As ListViewSortEventArgs) 
            Sort(e.SortExpression)
 
        End Sub
 

 

 

And the sort keeps a couple vars in viewstate

 

 

Private Sub Sort(ByVal sortexpression As String) 
            If _pageclicked And Not sortexpression Is Nothing Then
                ' just rebind 
                If ViewState("SortOrder") Is Nothing Then
                    Filtered = Filtered.OrderBy(Function(c As Car) GetCarSortProperty(sortexpression, c))
 
                Else
                    Filtered = Filtered.OrderByDescending(Function(c As Car) GetCarSortProperty(sortexpression, c))
                End If
 
            Else
                If Not sortexpression Is Nothing Then
                    If ViewState("SortExpression") Is Nothing OrElse Not ViewState("SortExpression").Equals(sortexpression) Then
                        ' first click  (on this header e.g. new sort)
                        Filtered = Filtered.OrderBy(Function(c As Car) GetCarSortProperty(sortexpression, c))
                        SetArrowImages(sortexpression)
                        ViewState("SortOrder") = Nothing
                        SetFirstPage()
 
                    Else
                        If ViewState("SortOrder") Is Nothing Then
                            ' second click
                            Filtered = Filtered.OrderByDescending(Function(c As Car) GetCarSortProperty(sortexpression, c))
                            SetArrowImages(sortexpression, False)
                            ViewState("SortOrder") = "Desc"
                        Else
                            ' third click- reset 
                            sortexpression = Nothing
                            SetArrowImages(sortexpression)
                            ViewState("SortOrder") = Nothing
 
                        End If
                    End If
                End If
 
                ViewState("SortExpression") = sortexpression
                _sorted = True
 
            End If
 
        End Sub

 

 

The GetCarSortProperty is nothing more than checking which sort (we are lucky here we only have two columns)

 

 

Private Function GetCarSortProperty(ByVal sort As String, ByVal c As Car) As Object
            Return IIf(sort.Equals("Name"), c.GetType().GetProperty("Name").GetValue(c, Nothing), IIf(sort.Equals("Location"), c.GetType().GetProperty("Location").GetValue(c, Nothing), Nothing))
        End Function

 

 

And of course the obligatory SetArrowImages

 

 

Private Sub SetArrowImages(ByVal sortexpression As String, Optional ByVal ascending As Boolean = True)
 
   Dim Asc As String = "~/_css/cars/icn_arrow_up.png"
   Dim Desc As String = "~/_css/cars/icn_arrow_down.png"
 
            Dim imgSortName As Image = New Image()
            Dim imgSortLocation As Image = New Image()
            If Not lv.FindControl("imgSortName") Is Nothing Then
                imgSortName = CType(lv.FindControl("imgSortName"), Image)
                imgSortLocation = CType(lv.FindControl("imgSortLocation"), Image)
            End If
 
            If Not sortexpression Is Nothing Then
                imgSortName.ImageUrl = IIf(sortexpression.Equals("Name"), IIf(ascending, Asc, Desc), Nothing)
                imgSortLocation.ImageUrl = IIf(sortexpression.Equals("Location"), IIf(ascending, Asc, Desc), Nothing)
 
                imgSortName.Visible = sortexpression.Equals("Name")
                imgSortLocation.Visible = sortexpression.Equals("Location")
 
            Else '   for third click
                imgSortName.Visible = False
                imgSortLocation.Visible = False
 
            End If
        End Sub

 

 

 

the cool parts are A) that we are generating the filter checkboxes dynamically, and B) the predicates...

Here's one of the categories of filter, called Colors, used in the GetData() at the top of this post

 

 

Where(ColorFilter()). _

 

 

And here's the function

 

 

 
Private Function ColorFilter() As Func(Of Car, Boolean)
            ' replicating PredicateBuilder...
            Dim t As Predicate(Of Car) = Function() True
            Dim f As Predicate(Of Car) = Function() False
            Dim cbl As List(Of CheckBox) = GetCheckBoxList(Me.lvColors)
 
            For Each cb As CheckBox In cbl.Where(Function(c As CheckBox) c.Checked)
                Dim cbstring As String = cb.Text.ToLower()
                f = PredicateExtensions.Or(f, Function(c As Car) c.ColorList.ToLower().Contains(cbstring))
            Next
            t = If(cbl.Any(Function(c) c.Checked), PredicateExtensions.And(t, f), t)
 
            Return t.ToFunction()
        End Function
 
        Private Function GetCheckBoxList(ByVal lv As ListView) As List(Of CheckBox)
            Dim result As List(Of CheckBox) = New List(Of CheckBox)
 
            For Each di As ListViewDataItem In lv.Items
                For Each c As Control In di.Controls
                    If (Not TryCast(c, CheckBox) Is Nothing) Then
                        result.Add(DirectCast(c, CheckBox))
                    End If
                Next
            Next
 
            Return result
        End Function
 

 

 

A couple of small details necessary for this page:

 

 

Private Sub SetFirstPage()
            Dim dp As DataPager = lv.FindControl("DataPager")
            If Not dp Is Nothing Then
                dp.SetPageProperties(0, dp.MaximumRows, False)
            End If
        End Sub
 
 
        Protected Sub btnSubmit_Click(sender As Object, e As System.EventArgs) Handles btnSubmit.Click 
            SetFirstPage()
            ViewState("SortExpression") = Nothing
            SetArrowImages(Nothing)
        End Sub
 
        Protected Sub PagePropertiesChanging(sender As Object, ByVal e As PagePropertiesChangingEventArgs)
            _pageclicked = True
 
        End Sub

 

 

 

I'm using a predicate extension above in ColorFilters() and the other checkbox functions, which is just a helper that saved me from importing a full blown predicatebuilder, but I had to translate it from c#.

 

 

 
Public Module PredicateExtensions
 
 
        <System.Runtime.CompilerServices.Extension()> _
        Public Function [And](Of T)(original As Predicate(Of T), newPredicate As Predicate(Of T)) As Predicate(Of T)
            Return Function(item) original(item) AndAlso newPredicate(item)
        End Function
 
        <System.Runtime.CompilerServices.Extension()> _
        Public Function [Or](Of T)(original As Predicate(Of T), newPredicate As Predicate(Of T)) As Predicate(Of T)
            Return Function(item) original(item) OrElse newPredicate(item)
        End Function
 
        ''' <summary>
        ''' Converts a Predicate into a Func(Of T, Boolean)
        ''' </summary>
        ''' <typeparam name="T"></typeparam>
        ''' <param name="predicate"></param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        <System.Runtime.CompilerServices.Extension()> _
        Public Function ToFunction(Of T)(ByVal predicate As Predicate(Of T)) As Func(Of T, Boolean)
            Dim result As Func(Of T, Boolean) = Function(item) predicate.Invoke(item)
            Return result
        End Function
    End Module
 

 

 

And that's it.

Tags:

Linq | VB.Net | ASP.Net

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

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