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
 
 
}
}

More js plugin fun, this time with with google maps

by MikeHogg 6. September 2012 09:40

I don't believe I've written this up anywhere and it might be useful to refer to in the future...  I did a rather extensive page in vb aspnet webforms that included mapping several markers on a google map.  Several lessons learned throughout.  Here is the mature code...

 

I was told to use a ListView control, and they wanted paging, so I hooked up the DataPager control to that, but they wanted the map to show everything in the list, not just the displayed page, so I just included a separate hidden listview, with my required data elements (all of them), no paging.  The data is a list of cars, each with a zip code as the sole location data.  I was to use this limited info to create markers for each car on the map.   Oh and the data was in a foreign language  :)  Everything else about the List should be standard here...

 

Added to the bottom of the html in this case...

<script language="javascript" type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=AIsomething&sensor=false"></script>
 <!-- deploy this script AFTER the maps api-->
 <script language="javascript" src="../_scripts/google-maps-3-vs-1-0.js" type="text/javascript"></script>
 <script language="javascript" type="text/javascript">
  var map; 
  var cars;
  var infowindow;
  var rollovers = [];
  var latlngprocessed = [];
  
  function init() {
   var mapOptions = {
    center: new google.maps.LatLng(51.55, 4.28), // default center
    zoom: 7,
    mapTypeId: google.maps.MapTypeId.ROADMAP
   };
               
   map = new google.maps.Map($("#CVMap")[0], mapOptions); 
 
   cars = getAllCars();
 
   $(cars).each(function () {
       if (!latLngAlreadyProcessed(this)) {                
           //  it's likely there exists more than one car at a particular latlng           
           var allmatches = getAllMatches(this);
 
           var image = new google.maps.MarkerImage('../_css/img/org_dwn_arrow.png',
                      new google.maps.Size(17, 19),
                      new google.maps.Point(0, 0),
                      new google.maps.Point(0, 19));
           var shadow = new google.maps.MarkerImage('../_css/img/org_dwn_arrow.png',
                      new google.maps.Size(40, 19),
                      new google.maps.Point(0, 0),
                      new google.maps.Point(0, 19));
           var shape = { coord: [1, 1, 1, 17, 19, 17, 19, 1],  type: 'poly' };
 
           var marker = new google.maps.Marker({
               map: map,
               position: getGoogleLatLng(this),
               shadow: shadow,
               icon: image,
               shape: shape,
               title: allmatches.length > 1 ? (allmatches.length) + ' cars' : this.titlelink.text
           });
 
           centerMap(allmatches);
           createInfoWindow();
 
           var $rollovercontent = $('<div class="carrollover" id="CarRollover"><h1></h1><ul></ul></div>');
           $.each(allmatches, function (idx, val) {
               var $item = $('<li></li>').html($(val.titlelink).clone())
        , $title = $('h1', $rollovercontent);
               if ($title.text() == '') {
                   $title.text('Cars in ' + val.location);
               }
               $('ul', $rollovercontent).append($item);
           });
 
           // closure and a separate array of rollovercontent needed here, because there is only one infowindow per map
           var i = rollovers.length; // get before push so we have index for closure below
           rollovers.push(getStringFromJqueryObject($rollovercontent));
 
           google.maps.event.addListener(marker, 'mouseover', (function (mark, idx) {
               return function () {
                   infowindow.setContent(rollovers[idx]);
                   infowindow.open(map, mark);
                   $('#CarRollover').parent().css('overflow-x', 'hidden');
                   setTimeout(function () {
                       $fix = $('#CarRollover').parent().parent();
                       $fix.css({'top' : '28px'});
                   }, 200);
               };
           })(marker, i));
       }
   });
  }

 

The closure is nothing more than creating distinct function instances on the fly for each rollover, since google maps only have one InfoWindow, we need to replace the content of it with that particular marker's info.  Also we merge info so a marker with several cars sharing the same zip code would show as a list of links.

The rest is just some standard helper jQuery-fu functions.

 

function centerMap(allmatches) { // to first in resultslist
      if ($.grep(allmatches, function (v) { return v.index == 0; }).length > 0) {
          map.setCenter(getGoogleLatLng(allmatches[0]));
      }
  }
 
  function createInfoWindow() {  // if not already created (google says only one per map)
      if (!infowindow) {
          infowindow = new google.maps.InfoWindow({
              maxWidth: 400
          });
      }
  }
  function getAllCars() {  
      var titlelinks = $("#hiddenformap .nameformap");
      var descriptions = $("#hiddenformap .shortdescriptionformap").map(function () { return $(this).text(); }).get();
      var locations = $(".hiddenlocationformap").map(function () { return $(this).text(); }).get();
      var latlngs = $(".hiddenlatlngformap").map(function () { return $(this).text(); }).get(); 
      var result = [];
      $.each(latlngs, function (idx, val) {
          if (val != '') {
              result.push({  
                        'index': idx,
                  'location': locations[idx],
                  'latlng': val,
                  'titlelink': titlelinks[idx],
                  'description': descriptions[idx]   });
          }
      });
   return result;
  }
 
  function getAllMatches(car) {
      return $.merge($.grep(cars, function (c) { return car.latlng == c.latlng && c != car; }), [car]);
  }
 
  function getGoogleLatLng(car) {
      return new google.maps.LatLng(car.latlng.split(",")[0], car.latlng.split(",")[1]);
  }
 
  function getStringFromJqueryObject(obj) {
      return $('<div>').append($(obj).clone()).html();
  }
 
        function latLngAlreadyProcessed(car) {
            var result = $.grep(latlngprocessed, function (ll) {
                return ll == car.latlng;
            }).length > 0;
            if (result == false) { latlngprocessed.push(car.latlng); }
            return result;
        }
One other thing to mention was that I was querying google for latitude and longitude and storing it server side, so as not to pound their geolocation service, as they requested in terms of service and by applying a few different limits.  So rather than sending zipcode (if you read the code above notice it should not send zip) it sends latlng which it already has.  Here is the server side code for google's geo service (note my XML library calls, which I now replace with 3.5 xml literals [yay] since I just found out about them)...

 

 

Private Function GetGoogleLatLng(ByVal c As Car, ByVal trynumber As Int16) As String
        Dim url As String = String.Format("http://maps.googleapis.com/maps/api/geocode/xml?sensor=false&address={0}", HttpUtility.HtmlEncode(j.FunctionLocationPostalCode))
        Dim x As XDocument = XDocument.Load(url)
        Dim s As XElement = x.Descendants("status")(0)
        If s Is Nothing Then
            'Logging.Log.
            Return String.Empty
        End If
 
        If s.Value = "OVER_QUERY_LIMIT" And trynumber < 4 Then
            System.Threading.Thread.Sleep(500) ' this sucks but what else can I do, actually it works well with google
            trynumber += 1
            Return GetGoogleLatLng(c, trynumber)
        End If
 
        Dim e As XElement = x.Descendants("geometry").Elements("location")(0)
        If Not e Is Nothing Then
            Return String.Format("{0},{1}", Xml.GetField(e, "lat", 25), Xml.GetField(e, "lng", 25))
        Else : Return String.Empty
        End If
 
    End Function 

 

 

BTW PS...

 

Want intellisense for your google. namespace?

 

I added this to the top of my usercontrol above

 

<%-- 
<% #if (false) %>
 <script src="../_scripts/jquery-1.7.1.min.js" type="text/javascript"></script>
<% #endif %>
--%>
 

Tags:

Javascript | JQuery

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

JQuery

by MikeHogg 5. July 2012 09:49

 

On my third web project, I got the opportunity to dive into jquery a little more.   We were rewriting an existing site in MVC3.  One of my tasks was to reproduce a standard sort of “Locations” page.  I had some javascript to work from, but we were adding a new “Location Features” feature, and a GPS feature, and the existing javascript wasn’t in any shape to be extended.  I only needed a small form, and my only server side code, my Location Features MVC3 call, was a three liner LINQ query against an Entity Framework db, so most of the task was my opportunity to rewrite some old mess of long cryptic javascript function into jquery, and figuring out a good readable, maintainable code flow.    In the first page here you will see my standard style of using the MVC model binding, even for two properties, and a very short clean page of html, with my CSS file referenced in the beginning, and my JS file referenced at the end.

 

 

 

@model SomeBase.Models.LocatorModel
<link rel="stylesheet" href="@Url.Content("~/Content/css/locator.css")">
<div class="locatordiv">
    @using (Html.BeginForm("Locator", "Home", FormMethod.Post, new { id = "locator" }))
    {   
        <p>  Enter a city and state, or zip code below.</p>
                    
        @Html.HiddenFor(m => m.Gps)
        @Html.TextBoxFor(m => m.Zip, new { title = "Enter ZIP", @Class = "textbox" })
        
        <input type="submit" value="" class="findbutton" data-category="Find" data-event="Homepage Zipcode" />
        <a href="#" class="gpsbutton" onclick="do_geo();" title="GPS" data-category="Find" data-event="GPS"></a>
     
        <div id="gpsloading">Working ...<br /><img src="@Url.Content("~/Content/images/load-bar.gif")" /></div>     
    }  
</div>
<div id="maploading">
    <img src="@Url.Content("~/Content/images/load-circle.gif")" />
</div>
<div id="results"></div>     
<div id="mapDiv"></div>
    
<script type="text/javascript">
            var configMQAPIKey = '@Html.Raw(System.Configuration.ConfigurationManager.AppSettings["MQ:APIKey"].ToString())';
            var configMQOLOKey = '@Html.Raw(System.Configuration.ConfigurationManager.AppSettings["MQ:OLOKey"].ToString())';
            var configMQHostedDataTable = '@Html.Raw(System.Configuration.ConfigurationManager.AppSettings["MQ:HostedDatatable"].ToString())'; 
        </script>
        <script src='//www.mapquestapi.com/sdk/js/v7.0.s/mqa.toolkit.js?key=@Html.Raw(System.Configuration.ConfigurationManager.AppSettings["MQ:APIKey"].ToString())'></script>
        <script src="@Url.SomeContent("~/Content/js/locator.js")"></script>
        <script type="text/javascript" src="@Url.Content("~/Content/js/jquery.cookie.js")" ></script> 

The javascript wasn’t terribly complicated, but it was fun to get deeper into jquery and learn to use standard jquery element creation and manipulation methods, and the unobstrusive javascript pattern, as opposed to the old verbose javascript getelementById calls.   Here you will see my style of paying special attention to method and variable names, in place of comments.  I was taught that comments should be for WHY not WHAT, and if you name your objects clearly enough, you don’t need to comment WHAT you are doing.

 

 

function searchByGps() {
    $("#Zip").val(""); $("#Key").val("")
    search("https://www.mapquestapi.com/search/v1/radius" +
                    "?key=" + mq_key + "&radius=" + $("#inradius").val() + "&callback=processPOIs&maxMatches=" + inmatch +
           "&origin=" + encodeURIComponent($("#Gps").val()) + "&hostedData=" + intable);
} 
function searchByZip() {
    $("#Gps").val(""); $("#Key").val("")
    search("https://www.mapquestapi.com/search/v1/radius" +
                    "?key=" + mq_key + "&radius=" + $("#inradius").val() + "&callback=processPOIs&maxMatches=" + inmatch +
           "&origin=" + encodeURIComponent($("#Zip").val()) + "&hostedData=" + intable);
} 
                
function search(url){
    MQA.IO.doJSONP(url); 
}
function processPOIs(results) {
    if (results.searchResults != null && results.searchResults.length > 0) {
        drawPOIsOnMap(results.searchResults);
        drawResultTable(results.searchResults);
        
        if ($("#Key").val()) {
            $("#Zip").val(results.searchResults[0].fields.address + " " + results.searchResults[0].fields.city + ", " + 
                          results.searchResults[0].fields.state + " " + results.searchResults[0].fields.postal); 
        }
        $.ajax('/Home/GetLocationFeatures', {
            data: JSON.stringify(parseToEntityObjects(results.searchResults)),
            dataType: "json",
            type: "post",
            contentType: "application/json",
            success: function (featuredata) { processFeatures(featuredata, results.searchResults); }
        });
    } 
    else if (results.info && results.info.statuscode == 610) {
        // ambiguities
        // results.collections[1] is To, 0 is From
        $("#Gps").val(results.collections[0][0].latLng.lat + ',' + results.collections[0][0].latLng.lng);
        searchByGps();
    }
    else {
        $('#results .frame').html("<h1>Oops! We couldn’t find any results. Please try your search again.</h1>");
    }
}
function processOLOs(oloData, searchResults) {
    $.each(searchResults, function (i, result) { 
        if (oloData != null) {
            $.each(oloData.restaurants, function () {
                if (this.telephone == result.fields.Phone) {
                    var oloid = '#olo' + result.fields.RecordId;
                    $(oloid).append($("<a></a>", { href: this.url, "class": "ololinks", target: "_blank", text: "Place an Order" }));
                }                    
            });  }   }); }  
function processFeatures(featureData, searchResults) {
    $.each(searchResults, function (i, result) { 
        if (featureData != null) {
            $.each(featureData, function () {
                if (this.StoreNumber == result.fields.RecordId) {
                    var fid = '#feature' + result.fields.RecordId;
                    $(fid).append($("<a></a>", {href:this.Url, "class":"featurelinks", target:"_blank", text:this.Label}));
                }   });         }  });  }   
function drawResultTable(results) {
    $("#maploading").hide();
    $('#results').html("<h1>Search Results</h1>");
         
    $.each(results, function (i, result) {
            
        $("<div></div>", { "class": 'resultrow' })
                     .append("<p class='addressrowresult'>" +
                result.fields.address + "<br/ >" +
                result.fields.city + ", " + result.fields.state + "<br/ >" +
                result.fields.Phone + "<br /></p>")
            .append("<div>" + getRoundedDistance(result) + " Mi.    " + getMapItLink(result) + "</div>") 
            .append("<div class='olos' id='olo" + result.fields.RecordId + "'></div>")   // olo can find this span$(#olo#storenumber#) later
            .append("<div class='features' id='feature" + result.fields.RecordId + "'></div>") // feature can find this span$(#feature#storenumber#) later
            .appendTo("#results");
    }); }  
function parseToEntityObjects(data) {
    var locations = [];
    $.each(data, function () {
        locations.push({
            StoreNumber: this.fields.RecordId,
            Address1: this.fields.address,
            City: this.fields.city,
            ZipCode: this.fields.postal,
            Phone: this.fields.Phone
        });
    });
    return locations;
}
function getRoundedDistance(result) { 
    if (result.distance > 10) {
        return Math.round(result.distance);
    } else if (result.distance > 1) {
        return Math.round(result.distance * 10) / 10;
    }  
    return Math.round(result.distance * 100) / 100; 
}
function getMapItLink(result){ 
    var destination = result.fields.address + ',' + result.fields.city + ',' + result.fields.state + ',' + result.fields.postal;
    var link = $("<a>", { href: "#", "class": "mapitlink", title: "Map It!", onclick: "mapIt(getOrigin(), '" + destination + "');return false;", text: "Map It!" })
                .attr({ "data-category": "Find", "data-event": "Map Quest" });
    return $('<div>').append(link.clone()).html();  // hack to get string not js object for mqa
}                    
function getPOIRollover(result) {
    var rollover = $("<div></div>", { "class": "poirollover" }).append($('<h4></h4>').text(result.fields.N))
        .append($('<span></span>').text(result.fields.address))
        .append($('<br />')).append($('<span></span').text(result.fields.city + ", " + result.fields.state))
        .append($('<br />')).append($('<span></span>').text(result.fields.Phone))
        .append($('<br />')).append($('<span></span>').text(getRoundedDistance(result) + " Mi.  "))
        .append(getMapItLink(result));
    return $('<div>').append(rollover.clone()).html(); // hack to get string not js object for mqa
}
function mapIt(origin, destination) {
    // build a url and send to the Directions Web Service
    MQA.IO.doJSONP("http://www.mapquestapi.com/directions/v1/route?" +
              "key=" + mq_key + "&" +
              "from=" + origin + "&" +
              "to=" + destination + "&" +
              "shapeFormat=raw&generalize=0.1&" +
              "callback=drawDirections");
}

 

My CSS file was mostly empty at the start.  But I wanted to make it easy to add style later, by applying classes and these empty placeholders for just about every element that we might want.

 

 

.locatordiv
{
    text-align:center; 
    margin:20px;
} 
.gpsbutton {
       background:url(../images/icon-gps.png) no-repeat;      
       background-size:42px auto; 
       padding:21px;    
} 
#gpsloading
{    display:none;
}
#maploading
{     display: none;  
}
#mapDiv
{    float:left; 
}
#results
{    float:left;
}
.resultrow
{    margin:20px;
}
.mapitlink
{    
}
.poirollover
{    width:150px;
}
.olos
{
}
.ololinks
{
}
.features
{    display:block;
}
.featurelinks
{    display:block;
}
.addressrowresult
{    width: 210px;
}
#directionsdiv
{    
}
.directionrow
{    
}

Tags:

Javascript | JQuery

AJAX and Javascript libraries

by MikeHogg 5. April 2007 09:44

AJAX is becoming a popular buzzword.  Prototyping is something I am just getting into, but for the most part I copy paste/adapt my personal js library functions in each new project I start  (as well as using free open sourced libraries from the internet).  Here is an example of some of my javascript in action.

    // BA.js
 //  modules for BA.aspx
 // mhogg jan07
   //---------------------
 // Functions-----------
 //  showSearch()                    three show() functions 
 //  showEmployee()                  ^
 //  showDependent(depID, EFID)      ^
 //  removeNavPath()                     show() helper
 //  addResultsLink()                    show() helper        
 //  setTitle(titleVal)                  show() helper
 //  showDiv(divID)                      show() helper
 //  LoadXMLDoc(url)                 requests employee data from server (1)
 //  blinkProgress()                     helper- blinks cute yellow box  (2)
 //  BuildXMLResults()                catches response                   (3)
 //  setData(rX)                      does something with data I forget  (4) calls next two functions
 //  setPeriodLinks()                creates date links on left side of page  (4a)
 //  fillDetails(instanceNum)        the meat of the page- uses two helper functions that follow this one (4b)
 //  clearData()                         helper- clears any values left from previous Instance 
 //  findOlderVal(pRoot, arrayNum)       helper- sets most recent value for a field in case of null value; always black
     //---------------------
 // Global vars
 var navPath; //initialized in showSearch()
 var tid;  // used for Progress BlinkBox
 var empXml; // this holds the Dataset Tables XML for one Employee
 var empName,empID; //set in fillDetails(), used in show() functions
 // the next two vars need to match each other for processing each XML value to a form field (span)
 var fieldArray = new Array('EIGname','EIGempid','EIGaddress','EIGcity'…
 var valArray = new Array('NAME','EID','ADDRESS','CITYANDZIP','HOME_PHONE'…
   //--------------------
 // Bodies
 function showSearch() {
     navPath = document.getElementById('divNavPath');  //first time initialization/ used in other functions
     showDiv('divFilterAndResultsList');
     setTitle('Search Results');
       empXml = null;
     setPeriodLinks();  //indirectly clears period links
     removeNavPath(); 
     navPath.appendChild(document.createTextNode('> Search Results ')); 
 }
   function showDependent(depID, EFID) {
     showDiv('divDependentInstance');
       //set values for each span
     //js xpath??
       if ( document.evaluate ) {  // W3C implemenation; else IE implementation     
         var xPathExp = "/NewDataSet/Table1[EFID=" + EFID + " and DEPID=" + depID + "]/DNAME";
         var xName = document.evaluate(xPathExp, empXml, null, XPathResult.ANY_TYPE, null );
         depName = xName.singleNodeValue.textContent;
       }else depName = empXml.selectNodes("/NewDataSet/Table1[EFID=" + EFID + " and DEPID=" + depID + "]/DNAME")[0].firstChild.nodeValue ;
       removeNavPath();
     addResultsLink();       
     node = document.createElement('a');
     node.href = 'javascript:showEmployee();';    
     node.appendChild(document.createTextNode(empName));
     navPath.appendChild(document.createTextNode(' > '));
     navPath.appendChild(node);
     navPath.appendChild(document.createTextNode(' > ' + depName));
       setTitle(depName);
     //setPeriodInstances for dependent
   }

 

So there was creating DOM manipulation, note the js XPath implementation that was actually easier in IE back then. That code breaks today since IE has become more compliant.  Here’s my ajax:

//-----------------------
 function LoadXMLDoc(url){ 
   if (window.XMLHttpRequest){ //Mozilla, Firefox, Opera 8.01, Safari, and now IE7?
     reqXML = new XMLHttpRequest(); 
     reqXML.onreadystatechange = BuildXMLResults; 
     reqXML.open("POST", url, true); 
     reqXML.send(null); 
   }
   else if(window.ActiveXObject){ // old IE
     reqXML = new ActiveXObject("Microsoft.XMLHTTP"); 
     if (reqXML) { 
       reqXML.onreadystatechange = BuildXMLResults; 
       reqXML.open("POST", url, true); 
       reqXML.send(); 
     } 
   }
   else{ //Older Browsers
     alert("Your Browser does not support XMLHttp!");
   }
   blinkProgress();
 } 
   function blinkProgress() {
     document.getElementById('inProgress').style.left = (document.body.clientWidth - 300) / 2;
     if (document.getElementById('inProgress').style.display=="none") {
         document.getElementById('inProgress').style.display="";
     } else document.getElementById('inProgress').style.display="none";
     tid = setTimeout('blinkProgress()', 500);
 }
  function BuildXMLResults(){  if(reqXML.readyState == 4){ //completed state    clearTimeout(tid);    document.getElementById('inProgress').style.display="none";    if(reqXML.status == 200){ //We got a success page back      if(reqXML.responseXML.documentElement && reqXML.responseXML.documentElement.getElementsByTagName('TABLE') != null){   //dummy test        //window.status = reqXML.responseXML; //display the message in the status bar                setData(reqXML.responseXML.documentElement);      }      else{        //Something's not right        alert("There was a problem retrieving the XML data:\nMissing ID; " + reqXML.responseText);      }    }     else{      //display server code not be accessed      alert("There was a problem retrieving the XML data:\nMissing 200; " + reqXML.responseText);    }              }}

We wrote our own progress bars back then, and animations, like here:

 

  // the search Results page slides up or down
 function toggleDisplay()  {
     //alert('display');  //debug
     // match gets an array, the second value ([1]) is what we are looking for- the value of display
     var cV = document.cookie.match ( 'display=(.*?)(;|$)' );
     if ( (cV) && cV[1] == "down" )  {
         document.cookie = "display=up";
         move("up");
         document.getElementById('ddlCsearch').style.display = ''; 
         document.getElementById('lbIsearch').style.display = ''; 
         document.getElementById('tblSearch').style.width = '800px';
     } else {
         document.cookie = "display=down";
         move("down");
         document.getElementById('ddlCsearch').style.display = 'none';  //ie6
         document.getElementById('lbIsearch').style.display = 'none';   //ie6 
         document.getElementById('tblSearch').style.width = '300px';
     }
 }
 function move(direction){
     //alert('#2:  move' +  document.getElementById('pnlResults').style.top);  //debug
     var currTop = document.getElementById('pnlResults').style.top.substr(0, document.getElementById('pnlResults').style.top.length - 2);
       if (direction == 'down') {
         if (currTop <= 30) {
             return;
         }else {
             document.getElementById('pnlResults').style.top = currTop - 30;
             setTimeout('move("down")',20);
         }
     }else {
         if (currTop >= 340) {
             return;
         }else {
             document.getElementById('pnlResults').style.top = eval(currTop) + 30;
             setTimeout('move("up")',20);
         }
     }
 }

Also our own validation:

 

  // ----- custom client side validation 
 function validate() {
   var badFields = '';
   var aTR = document.getElementsByTagName('TR');
   var aRowChecker = 0;
   var focusFlag = false;   
     for (var row=0;row<aTR.length;row++) {   //for each table row in form
     var cell = 0;
     while (aTR[row].childNodes[cell]){     // for each table cell in form
       var cellchild = 0;
         while (aTR[row].childNodes[cell].childNodes[cellchild]) {      // for each DOM obj in cell 
         var object = aTR[row].childNodes[cell].childNodes[cellchild];
         if (object.tagName == 'SELECT'){
           if (aTR[row].childNodes[0].innerHTML) {          // IE
             var myText = aTR[row].childNodes[0]
           } else { var myText = aTR[row].childNodes[1]}    //Mozilla 
              // Drop down validation
           if (object.id.indexOf('result') > 0 && 
             object.selectedIndex == 0) {
                 badFields += myText.innerHTML + '\n';
                 myText.style.color = 'red';
                 if (focusFlag == false){object.focus();focusFlag=true;}
           } else  {myText.style.color = 'black';}
         } 
         if (object.tagName == 'INPUT') {
           if (aTR[row].childNodes[0].innerHTML) {            // IE
             var myText = aTR[row].childNodes[0]
             } else { var myText = aTR[row].childNodes[1]}    //Mozilla 
              // textbox validation
           if (object.getAttribute('type') == 'text' && 
             object.value.length == 0 &&
             object.id.indexOf('result') > 0 &&
             object.maxLength < 200) {  //comments not a required field
               badFields += myText.innerHTML + '\n';
               myText.style.color = 'red';
               if (focusFlag == false){object.focus();focusFlag=true;}
           }  // Regex validation    
           else if ( object.getAttribute('type') == 'text'  &&  
               ( object.id.indexOf('result') > 0 || (object.id.indexOf('Q') == 0 && object.maxLength > 200) ) &&   
               !object.value.match(/^[-a-zA-Z,. @!()\''""?0-9]*$/)) { 
                   alert('Please use normal characters in ' + myText.innerHTML + object.value);                            
                   object.focus();
                   myText.style.color = 'red';
                   return false;
           }  // Radio validation   */
            else if (object.getAttribute('type') == 'radio') {
             var aRadio = document.getElementsByName(object.name);
             if(aRowChecker != row){                                             // aRowChecker used so we don't prompt user five times for each missed question
               if (myText.firstChild.nodeType == 1) {myText=myText.firstChild;}  // in case -style.color=red- applied, just get text
               if (!aRadio[0].checked && !aRadio[1].checked && !aRadio[2].checked && !aRadio[3].checked && !aRadio[4].checked){
                 badFields += myText.innerHTML + '\n';
                 myText.style.color = 'red';
                 aRowChecker=row;  
                 if (focusFlag == false){object.focus();focusFlag=true;}
               }   
               else {myText.style.color = 'black';}
             }
           } else  {myText.style.color = 'black';}
         }
         cellchild++;
       }  
       cell++;
     }
   } 
   if (badFields.length > 0) {
     alert('Please fill in the following fields:\n' + badFields);  
     return false;
   }
   else return true;
 }

My approach for browser compliance was to work in the major or main four browsers by using the simplest code possible.  I avoided hacks if I could. 

Tags:

Javascript

What’s wrong with this picture

by MikeHogg 20. November 2006 19:01

 

Here’s one of my first production web apps, a small Survey app that I designed and wrote in VB.Net.  This is funny to me, because I forgot that I ever used VB outside of college.  This was against Oracle… .Net 1.1 or 2.0

	Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
		'Put user code to initialize the page here
		Dim myQuery As New ArrayList
		Dim dctValidation As New Regex("^[0-9][0-9]-[JFMASOND][aepuco][nbrylgptvc]-[0-9][0-9]$")
		Dim txtValidation As New Regex("^[-a-zA-Z0-9 !@?\'"".,]+$")
		If Not IsPostBack Then
			GetInstructors()
			GetCourses()
			txtDCT.Text = DateTime.Now.ToString("dd-MMM-yy")
			GetQuestions("S", scaleRepeater)			' Scale Questions
			GetQuestions("C", commentRepeater)			' Comment Questions
		Else		'posting an Eval
			'Validation
			If Request.Form("txtFN").Length > 50 Or Request.Form("txtLN").Length > 50 _
			 Or Request.Form("ddlInstructor") <= 0 Or Request.Form("ddlCourse") <= 0 _
			 Or Request.Form("ddlInstructor") > 9999999 Or Request.Form("ddlCourse") > 9999999 _
			 Or Not dctValidation.IsMatch(Request.Form("txtDCT")) Or Not txtValidation.IsMatch(Request.Form("txtFN")) _
			 Or Not txtValidation.IsMatch(Request.Form("txtLN")) Then
				lblMisc.Text = "We have a problem here, missing some required information.  <br>" & _
				 "Enable javascript for details and use your browser's back button to return to the Evaluation."
			Else			 ' valid Eval, post transaction to db
				myQuery.Add("insert into surveys (SURVEY_ID, STUDENT_LAST_NAME, STUDENT_FIRST_NAME, COURSE_DATE," & _
				  " INSTRUCTOR_ID, COURSE_ID, UPDATE_USERID, UPDATE_DATE) values(SURVEY_ID_SEQ.NEXTVAL, '" & _
				  Request.Form("txtLN").Replace("'", "''") & "', '" & _
				  Request.Form("txtFN").Replace("'", "''") & "','" & Request.Form("txtDCT") & "','" & _
				  Request.Form("ddlInstructor") & "','" & Request.Form("ddlCourse") & "', " & _
				  "'WebUser', SYSDATE)")
				'POST method relies on Request.forms key names to put query together- 
				'  all number keys have types as values, 
				'  the Qx keys have the answers as values.
				Dim field As String
				Dim myQ As Regex
				For Each field In Request.Form.AllKeys
					If myQ.IsMatch(field, "[1-9][0-9]?") Then					' (one or two digit number)
						If Request.Form(field) = "S" Then
							myQuery.Add("insert into SURVEY_SCALED_ANSWER (SCALE_ANSWER_ID, SURVEY_ID," & _
							   "QUESTION_ID, SCALE_VALUE, UPDATE_USERID, UPDATE_DATE) values (" & _
							   "SCALED_ANSWER_ID_SEQ.NEXTVAL, SURVEY_ID_SEQ.CURRVAL, " & field & _
							   ", '" & Request.Form("Q" & field) & "', 'WebUser', SYSDATE)")
						ElseIf Request.Form(field) = "C" And Not Request.Form("Q" + field) = "" Then
							myQuery.Add("insert into SURVEY_COMMENTS (COMMENT_ID, SURVEY_ID," & _
							 "QUESTION_ID, COMMENT_DESC, UPDATE_USERID, UPDATE_DATE) values (" & _
							 "COMMENTS_ID_SEQ.NEXTVAL, SURVEY_ID_SEQ.CURRVAL, " & field & _
							 ", '" & Request.Form("Q" & field).Replace("'", "''") & "', 'WebUser', SYSDATE)")
						End If
					End If
				Next
				If UpdateTables(myQuery) Then				' transaction processed
					Response.Write("Your evaluation has been recorded.  <p>Your feedback is important, so that OIT can continue to provide <br>Baltimore County Employees with the best possible training. </p> <p>Thank you." & _
				 "</p><p><input type='button' id='btnEnd' onclick='javascript:window.location.href=""/TE""'" & _
				 " value='Fill in another Evaluation' /></p>")
					pnlForm.Visible = False
				End If
			End If
		End If
	End Sub

 

I was really proud of writing this dynamic questions Query array processing routine, instead of hardcoding 20 or 30 inserts, or however many questions were on the survey, but one glaring redball stares at me when I review this code- SQL injection.  Thankfully, this was an intranet web app. 

I was also surprised to read my documentation, now years later, and see how superior it is to my current documentation Smile

TrainEval Application Notes 
** For production implementation:
** 1. zip the entire TE directory
** 2. remove the two (2) web.config files from the zip archive
** 3. remove the TE/Admin/FormsAuth/Users.xml file from the zip archive
** 4. unzip to intranetprod/wwwroot
** 5. enjoy!
audience: developers, troubleshooters
purpose: describe the app in summary, pointing out the main functions and their locations
_______________
TE/default.aspx
---------------
...provides the actual evaluation that users fill out.
custom javascript validation (regex) and server-side validation (regex).
 The client side custom js validation is rather cumbersome, and although it was written with
 multiple browsers in mind and the possibility of different questions or numbers of questions, it 
 should be pretty robust if Evals change at all.
 
a couple other javascript features are included at the bottom of the Eval.aspx html.
uses js.calendar (needs 4 files in directory, see <script> includes in html page)- 
Gets questions from Oracle DB dynamically.  In order to change questions, add or remove, 
 all that needs to be done is to edit the database.  The app will display only questions 
 and text that are active (Active Flag), and display them according to their type- S or C.
 
___________________
TE/Admin/Admin.aspx
-------------------
....provides Edit and Reporting functionality for somebody and one assistant.
uses js.calendar as above.
uses graphics/ directory for button images.
stores Crystal Reports in crystal directory but doesn't use these copies.
uses FormsAuth directory (need to remove read permissions from all other users when implementing)
four sections: 
  Instructors  	(updateable repeater)
  Courses	(updateable repeater)
  Edit Evals	(search form and meat of the program- see below)
  Reports	(search form and two links to Crystal reports)
  
The Instructors and Courses are simple repeaters, allowing changes to Active status and 
 updates to names.  New items can be added at the bottom of each repeater (footer).  I added a filter
 system of alphabet buttons to the Courses page to make it quicker (big improvement) and more user friendly.
 
Reports uses Crystal Enterprise.  This is straightforward.  Couldn't implement the 'Most Recent # Evals' search 
 parameter with Crystal, though.
The Edit Evals section uses httprequest object (AJAX) to display individual results (each eval) without posting back
 each time you page through them.  All Evals are returned and held in Dataset on server when queried, then one at a time is 
 sent to the client as you page through.  This got kind of tricky with stuff like keeping the page count 
 and returning to the same page after an update.  Page count is held in lblMisc... display status (toggle up/down) is 
 held in cookie... and all features in regular Eval are available- validation, Overall Autocalculate,
 and each Eval can be resubmitted (Submit Changes).  One main difference between this page and Evals page is that this 
 page also includes all Instructors and Courses, not just Active ones.  
  
_____________________
Diffs of Dev and Prod
---------------------
 
 To promote to production only the webconfig needs to be changed.

 

I had some neat features in this project.  Not only was it an application for internal members who had taken a course to fill out a survey on an instructor, but it also included the Administrator back end to select subsets of data based on instructor names or courses and then page through the results one by one or view a Crystal Report in the browser.  Here I was constructing controls dynamically, some with fancy mouseovers, another a filter of all the letters of the alphabet, that the Admin could click on to view only surveys on instructors with names ending with that letter.  The Admin Area was written in c#:

        // Setting controls
        private void setButtonsAndPanels(ImageButton btnSelected)
        {
            ImageButton[] ibCollection = { btnInstructors, btnCourses, btnEditEvals, btnReports };
            foreach (ImageButton myButton in ibCollection) {
                if (myButton != btnSelected) {
                    myButton.Attributes.Add("onmouseover","this.src='graphics/btnInv" + myButton.ID.Remove(0,3) + ".gif';");
                    myButton.Attributes.Add("onmouseout","this.src='graphics/btn" + myButton.ID.Remove(0,3) + ".gif';");
                    myButton.ImageUrl="graphics/btn" + myButton.ID.Remove(0,3) + ".gif";
                }
                else {
                    myButton.Attributes.Remove("onmouseover");
                    myButton.Attributes.Remove("onmouseout");
                    myButton.ImageUrl="graphics/btnSel" + myButton.ID.Remove(0,3) + ".gif";
                }
            }
            
            Panel[] pCollection = { pnlCourses, pnlInstructors, pnlEditEvals, pnlReports, pnlSearch, pnlResults};
            foreach (Panel myPanel in pCollection) {
                if (myPanel.ID.Remove(0,3) != btnSelected.ID.Remove(0,3)){
                    myPanel.Visible = false;
                }
                else myPanel.Visible = true;
            }
        }
        private void loadAlphaButtons() {
            LinkButton myButton;
            int counter;
            char letter;
            try {
                pnlAlpha.Controls.AddAt(0,new LiteralControl("Filter: "));
                for (counter = 0;counter <=25;counter++) {
                    letter = (char) (counter+65);
                    myButton = new LinkButton();
                    pnlAlpha.Controls.AddAt( (counter * 2) + 1, myButton);
                    myButton.ID = letter.ToString();
                    myButton.Text = letter.ToString();
                    myButton.ForeColor = Color.Blue;
                    
                    //if (counter < 25) {
                    pnlAlpha.Controls.AddAt(pnlAlpha.Controls.IndexOf(myButton) + 1, new LiteralControl("&nbsp;|&nbsp;"));
                    //}
                    myButton.Click += new System.EventHandler(this.btnAlpha_Click);
                }
                LinkButton btnClear = new LinkButton();
                btnClear.Text = "Clear";
                btnClear.Click += new System.EventHandler(this.btnClear_Click);
                pnlAlpha.Controls.AddAt(53, btnClear);
            }
            catch (Exception ex) {
                throwEx(ex.ToString(), ex.Source.ToString());
            }
        }

This was also the beginnings of my personal library functions.  I believe Logging to be a standard feature, and a developer needs of course the boilerplate Data Access library, as well as some standard exception handling techniques.  All of which grew over time, but these were my first techniques…

 {
            DataSet myDS = new DataSet();
            try {
                OracleDataAdapter oraAdp = new OracleDataAdapter(myQuery, oraConn);
                oraAdp.Fill(myDS);
                myRepeater.DataSource=myDS.Tables[0];
                myRepeater.DataBind();
            } 
            catch (Exception ex){
                if (oraConn.State == ConnectionState.Open){ 
                    oraConn.Close();
                }
                throwEx(ex.ToString(), ex.Source.ToString());
            }  
            finally 
            {
                if (oraConn.State == ConnectionState.Open)
                { 
                    oraConn.Close();
                }
            }          
        }
        // Helpers
        private bool updateTables( ArrayList pQuery )
        {
            try {
                OracleCommand oraCmd = new OracleCommand();
                try {
                    oraConn.Open();
                } catch (Exception ex){
                    writeLog(ex.ToString(), ex.Source.ToString());
                    oraConn.Close();
                    oraConn.Open();
                }
                    
                OracleTransaction tran = oraConn.BeginTransaction();
                oraCmd.Connection = oraConn;
                oraCmd.Transaction = tran;   // necessary for MS OracleClient
                try {
                    foreach (string myQ in pQuery){
                        oraCmd.CommandText = myQ;
                        if (myQ != null){
                            oraCmd.ExecuteNonQuery();
                        }
                    }               
                tran.Commit();
                return true;
                }
                catch (Exception ex) {
                    tran.Rollback();
                    throwEx(ex.ToString(), ex.Source.ToString());
                    return false;
                }
            }
            catch (Exception ex){
                throwEx(ex.ToString(), ex.Source.ToString());
                return false;
            }
            finally  {
                if (oraConn.State == ConnectionState.Open) { oraConn.Close();}
            } 
        }
    
        private void throwEx( string errorMess, string sender ) 
        {
            hidePanels(this);
            lblMisc.Text = "<br>We're sorry, the operation you have attempted was not successful.  Use your browser's" +
            " Back Button and try again and if you are still not successful, then call the Help Desk at 8200.";
            lblMisc.Visible = true;
            writeLog(errorMess, sender);
        }
        private void writeLog( string exMsg, string sender ) 
        {
            DateTime time = DateTime.Now;
            FileStream fs = new FileStream(Server.MapPath("logs/errlog.txt"), FileMode.OpenOrCreate, FileAccess.Write);
            StreamWriter s = new StreamWriter(fs);
            s.BaseStream.Seek(0, SeekOrigin.End);
            s.WriteLine(time.ToString() + ":" + sender + ":" + exMsg);
            s.Close();
        }

I also didn’t learn about JSON until later, but still got by using XML to pass data and messages back and forth from server to a web page to change DOM elements on the fly.  Here a server method…

        //      xmlhttprequest interaction 
        private void getResultRow(int pageNum){
            try {
                DataTable dt = new DataTable();
                dt = (DataTable)Cache["myX" + Session.SessionID];
                DataRow dr = dt.Rows[pageNum];
                
                StringBuilder xResult = new StringBuilder("<Survey>");
                foreach ( DataColumn c in dt.Columns ) {
                    string colName = c.ColumnName;
                    string colVal = dr[colName].ToString().Replace("&","&amp;");   // xml issue with &s
                    xResult.Append("<" + colName + ">" + colVal + "</" + colName + ">");
                }
                xResult.Append("</Survey>");
                
                Response.ContentType = "text/xml";
                Response.Write(xResult.ToString());
                Response.End();
            }
            catch (Exception ex){
                if ( ex.GetBaseException().GetType().Name == "ThreadAbortException" ) {
                    return;
                }
                throwEx(ex.ToString(), ex.Source.ToString());
            }
        }

The javascript was fun to write.  I did a lot of DOM manipulation:

var myPage, pageTotal;
function getPage(pageNum) {
    //alert('getPage');  //debug
    // NOTE: pageNum/myPage comes from pagebuttons already inc/decremented.  
    // this function is also called by server in btnUpdate_click where pageNum is lost so...
    myPage = pageNum;  
    pageTotal = document.getElementById('lblPageTotal').innerHTML;
    
    if (pageNum >= 0) {
        if (pageNum < pageTotal) {
            document.getElementById('lblPageCount').innerHTML = (pageNum + 1);
            LoadXMLDoc("Admin.aspx?Page=" + pageNum);   
        } else {alert('Page ' + pageNum + ': No next page');myPage--;} //set increment back
    } else {alert('Page 1:Cannot go back'); myPage++;}  // set decrement back
}
var reqXML;
function LoadXMLDoc(url){ 
  //alert(url);       //debug
  if (window.XMLHttpRequest){ //Mozilla, Firefox, Opera 8.01, Safari, and now IE7?
    reqXML = new XMLHttpRequest(); 
    reqXML.onreadystatechange = BuildXMLResults; 
    reqXML.open("POST", url, true); 
    reqXML.send(null); 
  }
  else if(window.ActiveXObject){ //IE
    reqXML = new ActiveXObject("Microsoft.XMLHTTP"); 
    if (reqXML) { 
      reqXML.onreadystatechange = BuildXMLResults; 
      reqXML.open("POST", url, true); 
      reqXML.send(); 
    } 
  }
  else{ //Older Browsers
    alert("Your Browser does not support Ajax!");
  }
  blinkProgress();
} 
var tid
function blinkProgress() {
    document.getElementById('inProgress').style.left = (document.body.clientWidth - 200) / 2;
    //alert('blink');  //debug
    if (document.getElementById('inProgress').style.display=="none") {
        document.getElementById('inProgress').style.display="";
    } else document.getElementById('inProgress').style.display="none";
    tid = setTimeout('blinkProgress()', 500);
}
function BuildXMLResults(){
  if(reqXML.readyState == 4){ //completed state
    clearTimeout(tid);
    document.getElementById('inProgress').style.display="none";
    if(reqXML.status == 200){ //We got a sucess page back
      if(reqXML.responseText.indexOf("ID") >= 0){   //dummy test
        //window.status = reqXML.responseXML; //display the message in the status bar
        //alert('Success: \n' + reqXML.responseText);   //debug
        setData(reqXML.responseXML.documentElement);
      }
      else{
        //Something's not right
        //alert('XML:\n' + reqXML.responseXML + 'Text:\n' + reqXML.responseText);   //debug 
        alert("There was a problem retrieving the XML data:\n" + reqXML.statusText);
      }
    } 
    else{
      //display server code not be accessed
      //alert('readyState: ' + reqXML.readyState + '\nstatus: ' + reqXML.status + 'responseText: ' + reqXML.responseText);   //debug
      alert("There was a problem retrieving the XML data:\n" + reqXML.statusText);
    }		
  }
}
//----- fills in Eval with answers from XML response, uses the three functions following this one
function setData(rX){
    clearData();
    
    if(rX == null) {
        alert('An error has occured, setData did not receive any data');
        return;
    }
    for(var c=0;c<rX.childNodes.length;c++){
        var myElement = rX.childNodes[c].nodeName;
        if (rX.childNodes[c].textContent){
            var myText = rX.childNodes[c].textContent;  // for diff DOMS
        }else var myText = rX.childNodes[c].text;       // for diff DOMS
        switch(myElement) {
            case "SURVEY_ID" : document.getElementById('txtIDresults').value = myText;break;
            case "FIRST_NAME" : document.getElementById('txtFNresults').value=myText;break;
            case "LAST_NAME" : document.getElementById('txtLNresults').value=myText;break;
            case "DATE_COURSE_TAKEN" : document.getElementById('txtDCTresults').value=myText;break;
            case "INSTRUCTOR" : setDDL("ddlIresults",myText);break; 
            case "COURSE" : setDDL("ddlCresults",myText);break;
            default: if (myElement.indexOf("Q")==0) {
                setQ(myElement, myText);
                }
        }
    }
    // set any reds from a previous validation to black again
    var aTD = document.getElementsByTagName('TD');    
    var cellNum = 0;
    while ( aTD[cellNum] ) {
        if ( aTD[cellNum].style && aTD[cellNum].style.color && aTD[cellNum].style.color == 'red' ) {
            aTD[cellNum].style.color = 'black';
        }
        var cellChild = 0;
        while ( aTD[cellNum].childNodes[cellChild] ){
            var obj = aTD[cellNum].childNodes[cellChild];
            if ( obj.style && obj.style.color && obj.style.color == 'red' ) {
                obj.style.color = 'black';
            }
            cellChild++;
        }
        cellNum++;
    }
}
//----- clears any values left from previous Eval
function clearData() {
  for (c=0; c < document.forms[0].length; c++) {
    ele = document.forms[0].elements[c];
    if ( ele.getAttribute('type') == 'text' && ele.id.indexOf('search') < 0 ) {
        ele.value = '';
    }
    else if ( ele.getAttribute('type') == 'radio' ) {
        ele.checked = false;
    }
    else if ( ele.tagName == 'SELECT' && ele.id.indexOf('search') < 0 ) {
        ele.selectedIndex = 0;
    }
  } 
}
//----- makes Course and Instructor Drop Down List selections
function setDDL(aDDLName, sText){
    //alert(sText);   //debug
    var aDDL = document.getElementById(aDDLName);
    //alert('len = ' + aDDL.length + '\nsText = ' + aDDL.options[2].text);   //debug
    for(var c = 0;c < aDDL.length;c++){
        if (sText == aDDL.options[c].value){
            aDDL.selectedIndex = c;
        }
    }
}
//----- fills in scale and comment answers
function setQ(qNum, answer){
    var qEle = document.getElementsByName(qNum);
    if (qEle[4]){    // radio question
        if (answer < 6 && answer > 0) {  // test valid answer
            for(var c = 0;c < 5;c++){
                if (qEle[c].value == answer){
                    qEle[c].checked = true;
                }
            }
        }
    } else {qEle[0].value = answer;}  //Comment Question
}

 

I also did a fair amount of animation…

// the search Results page slides up or down
function toggleDisplay()  {
    //alert('display');  //debug
    // match gets an array, the second value ([1]) is what we are looking for- the value of display
    var cV = document.cookie.match ( 'display=(.*?)(;|$)' );
    if ( (cV) && cV[1] == "down" )  {
        document.cookie = "display=up";
        move("up");
        document.getElementById('ddlCsearch').style.display = ''; 
        document.getElementById('lbIsearch').style.display = ''; 
        document.getElementById('tblSearch').style.width = '800px';
    } else {
        document.cookie = "display=down";
        move("down");
        document.getElementById('ddlCsearch').style.display = 'none';  //ie6
        document.getElementById('lbIsearch').style.display = 'none';   //ie6 
        document.getElementById('tblSearch').style.width = '300px';
    }
}
function move(direction){
    //alert('#2:  move' +  document.getElementById('pnlResults').style.top);  //debug
    var currTop = document.getElementById('pnlResults').style.top.substr(0, document.getElementById('pnlResults').style.top.length - 2);
    if (direction == 'down') {
        if (currTop <= 30) {
            return;
        }else {
            document.getElementById('pnlResults').style.top = currTop - 30;
            setTimeout('move("down")',20);
        }
    }else {
        if (currTop >= 340) {
            return;
        }else {
            document.getElementById('pnlResults').style.top = eval(currTop) + 30;
            setTimeout('move("up")',20);
        }
    }
}

Tags:

VB.Net | Javascript | C# | ASP.Net

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