jQuery Autocomplete Inconsistencies

April 17, 2014

I guess I found an inconsistency in jQuery’s autocomplete() widget, which does not cause a selected value to be reset when clearing the input field.

Steps to reproduce (checked with 1.8.3 and 1.11.0):

  • Type the beginning of a string that is provided by the autocomplete source
  • Tab away from the input
  • “autocompletechange” fires, but the value is not valid, so the input field value is reset
  • Type the beginning of string (as above)
  • Select an entry using cursor keys
  • “select” fires (and sets the corresponding key value for the selected text value in a callback function)
  • Double-click the input to select the full text of the input field
  • Delete selected text (backspace or right-click Cut)
  • Neither “autocompletechange” nor “select” fires
  • Tab away from input field
  • Neither “autocompletechange” nor “select” fires

From the console.log:

blur ” 5:81
mouseup ” 5:90
keyup ‘r’ 5:87
keyup ‘re’ 5:87
keyup ‘rei’ 5:87
keyup ‘re’ 5:87
change ‘re’ 5:84
autocompletechange null 5:78
Nothing selected, input was re common.js:114
blur ” 5:81
mouseup ” 5:90
keyup ‘re’ 5:87
keyup ‘re’ 5:87
keyup ‘rei’ 5:87
keyup ’6749′ [this is the key value!] 5:87
Selected: [some text] (6749) common.js:114
keyup ‘[some text] 5:87
keyup ‘[some text] 5:87
keyup ‘[some text] 5:87
keyup ” 5:87
change ” 5:84
blur ”

The value displayed in single quotes is the .value property of the input field, “rei” being the string that corresponds to the beginning of a valid value in the source.

The only workaround that comes to mind is to use the keyup and mouseup events to check whether the input’s value property is empty, and then reset the key field accordingly.


A jQuery Alternative for ACT CalendarExtender

April 6, 2014

As I extend an ASP.Net application with MVC functionality, I also need to replace some of the components that the AjaxControlToolkit provides. Case in point, the CalendarExtender.

The CE is declared in markup like this:

<ajax:CalendarExtender id=”ce” runat=”server” TargetControlID=”edDate” Format=”dd.MM.yyyy”>

edDate being the ASP.Net TextBox control containing the selected date.

The simplest replacement is, of course, the Datepicker widget found in the jQuery UI library.

Integration the Datepicker is a bit more effort as compared to the other controls in this series, but not much.

First we need to reference the required .js and .css files of jQuery and jQuery UI:

<script type="text/javascript" src="@Url.Content("~/script/jquery-1.8.3.js")"></script>
<script type="text/javascript" src="@Url.Content("~/script/jquery-ui-1.9.2.custom.min.js")"></script>
<link href="@Url.Content("~/css/jquery-ui-1.9.2.custom.css")" rel="stylesheet" type="text/css" />

Depending on the options you used for the CalendarExtender, the initialization for the Datepicker may look like this:

$("#edDate").datepicker({
  changeMonth: true,
  changeYear: true,
  showButtonPanel: true
});
$("#edDate").datepicker("option", "dateFormat", "dd.mm.yy");
$("#edDate").datepicker("option", $.datepicker.regional["de"]);

For more options, see the sample page and press View Source, or browse the documentation.

Note that while the CE requires “yyyy” for 4-digit years, the Datepicker interprets “yy” as 4-digit years, and will insert the year twice if you use “yyyy”.

I’m not sure whether the localization files are part of the default jQuery UI downloads – if not, you can find the localized strings for Datepicker in the GitHub repository.

Lastly, the CE provides a “Today” button which sets the current date and closes the calendar, whereas Datepicker just navigates to the current date in the calendar control, but does not select it.

To change the Datepicker behavior, I use the code found in an answers on SO:

$.datepicker._gotoToday = function (id) {
    var target = jQuery(id);
    var inst = this._getInst(target[0]);
    if (this._get(inst, 'gotoCurrent') && inst.currentDay) {
        inst.selectedDay = inst.currentDay;
        inst.drawMonth = inst.selectedMonth = inst.currentMonth;
        inst.drawYear = inst.selectedYear = inst.currentYear;
    }
    else {
        var date = new Date();
        inst.selectedDay = date.getDate();
        inst.drawMonth = inst.selectedMonth = date.getMonth();
        inst.drawYear = inst.selectedYear = date.getFullYear();
        this._setDateDatepicker(target, date);
        this._selectDate(id, this._getDateDatepicker(target));
    }
    this._notifyChange(inst);
    this._adjustDate(target);
};

See also here for further discussion.

Finally, if the jQuery UI skins do not fit in the design of your application, open the developer tools of your browser (F12) and find out which jQuery UI CSS classes define the look of the Datepicker control. In my case, I added a bit of CSS to adjust the control:

.ui-datepicker table.ui-datepicker-calendar {
    font-size: 11px;    
}
.ui-datepicker table.ui-datepicker-calendar td
{
    width: 17px;
}
.ui-datepicker table.ui-datepicker-calendar td a 
{
    padding: 0px;
}
.ui-datepicker .ui-datepicker-title select {
    font-size: 0.9em;
    margin: 1px 0;
}
.ui-widget button {
    font-size: 0.8em;
}

Note that the overriding style definition selectors must be more specific than the original jQuery UI declarations.

Aaaand we are done! ;)


Batch Unblock Files

April 2, 2014

If you download a file, or receive a file by email, and save it to disk, the file is marked as downloaded from an external computer, and cannot be executed or, depending on the application, opened. You need to open the file properties dialog, and click “Unblock” to resolve the situation.

The same is true for zip files and other archives: When extracting the archive, the “blocked” flag may be set on every extracted file, and the “Unblock” button is not available if you select more than one file and open the properties dialog.

Fortunately, there is an easy solution that I found in a comment on SO:

for /F %a in ('dir /r/b/s') do @echo .>%a:Zone.Identifier:$DATA

 


A jQuery Alternative for ACT HoverMenuExtender

April 1, 2014

As I extend an ASP.Net application with MVC functionality, I also need to replace some of the components that the AjaxControlToolkit provides. Case in point, the HoverMenuExtender.

The HME is declared in markup like this:

<ajax:HoverMenuExtender ID=”hme” runat=”server” TargetControlID=”tdTable” PopupControlID=”paTable” PopupPosition=”Bottom” OffsetX=”0″ OffsetY=”2″>
</ajax:HoverMenuExtender>

tdTable being, in my case, a TD table cell in a menu bar, and paTable the submenu that is meant to dropdown when hovering over the table cell.

As replacement, I found the Simple jQuery Dropdowns library on CSS-Tricks, and the sample looked similar to the solution I wanted to replace.

Adding the Simple Dropdowns was straight forward, as it consists only of a couple of Javascript lines and some CSS classes. Integration was easy and quickly done.

But then…

As I mentioned, the menu bar was defined as single-row HTML table, and the cells are fixed-width for all drop-down menus, except the one displaying a record identifier – this cell uses a table’s (built-in!) capability to expand so that the table extends over the defined width, no matter how many other fixed-width cells there are and how wide the browser window is.

So I added some display:table* styles like this

<ul class="dropdown" style="width: 99%; display:table">
    <li style="display:table-cell; ">
        <a href="#">Menu 1</a>
        <ul class="sub_menu">
            <li><a href="#">Command 1</a></li>
            <li><a href="#">Command 2</a></li>
        </ul>
    </li>
    <li style="display:table-cell; width: auto" >
        <a href="#">ID</a>
        <ul class="sub_menu">
            <li><a href="#">Command 1</a></li>
            <li><a href="#">Command 2</a></li>
        </ul>
    </li>
      <li style="display:table-cell; ">
        <a href="#">Menu 2</a>
        <ul class="sub_menu">
            <li><a href="#">Command 1</a></li>
            <li><a href="#">Command 2</a></li>
        </ul>
    </li>
    <li style="display:table-cell">&nbsp;</li>
</ul>

Chrome and IE9 displayed the menu as expected, but Firefox insisted on displaying the dropdown left-aligned below to <ul> table, no matter how I changed the individual styles and classes.

Fortunately I found this answer on SO that illustrated a solution that works for Firefox (and the other browsers!): add a <div style=”position:relative”> inside the <td>.

So now my menu bar looks like this:

<ul class="dropdown" style="width: 99%; display:table">
    <li style="display:table-cell; " >
        <div style="position: relative">
            <a href="#">Menu 1</a>
            <ul class="sub_menu">
                <li><a href="#">Command 1</a></li>
                <li><a href="#">Command 2</a></li>
            </ul>
        </div>
    </li>
    <li style="display:table-cell; width: auto" >
        <div style="position: relative">
            <a href="#">ID</a>
            <ul class="sub_menu">
                <li><a href="#">Command 1</a></li>
                <li><a href="#">Command 2</a></li>
            </ul>
        </div>
    </li>
    <li style="display:table-cell; " >
        <div style="position: relative">
            <a href="#">Menu 2</a>
            <ul class="sub_menu">
                <li><a href="#">Command 1</a></li>
                <li><a href="#">Command 2</a></li>
            </ul>
        </div>
    </li>
    <li style="display:table-cell" class="command">&nbsp;</li>
</ul>

A jQuery Alternative for ACT CollapsiblePanelExtender

March 30, 2014

As I extend an ASP.Net application with MVC functionality, I also need to replace some of the components that the AjaxControlToolkit provides. Case in point, the CollapsiblePanelExtender.

The CPE is declared in markup like this:

<ajax:CollapsiblePanelExtender ID=”cpe” runat=”server” CollapseControlID=”paHeader” ExpandControlID=”paHeader” AutoCollapse=”false” AutoExpand=”false” Collapsed=”true” TargetControlID=”paDetail” ExpandedImage=”~/images/collapse_blue.jpg” CollapsedImage=”~/images/expand_blue.jpg” SuppressPostBack=”true” ImageControlID=”imgHeader”>
</ajax:CollapsiblePanelExtender>

paHeader and paDetail are both asp:Panels – clicking on the header toggles the display of the detail panel. The image control is toggled accordingly.

My goal was to create a JavaScript function which uses parameters similar to the markup, omitting those that do not matter in my application.

In MVC, of course, the equivalent of an asp:Panel is a simple HTML <div>.

I started out with a function declaration like this (please forgive non-standard casing):

function CollapsiblePanelExtender(ID, CollapseControlID,
    TargetControlID, ImageControlID) { }

but then noticed that I needed to distinguish between initial states expanded and collapsed, as well as an additional callback after expanding/collapsing.

var CollapsiblePanelExtenders = {};
function CollapsiblePanelExtender(ID, CollapseControlID, 
    TargetControlID, ImageControlID, expanded, fn) {
  var ExpandedImage = '@Url.Content("~/images/collapse_blue.jpg")';
  var CollapsedImage = '@Url.Content("~/images/expand_blue.jpg")';

CollapsiblePanelExtenders is our array of all CPEs on the page. Parameters expanded and fn are optional.

First, we register the CPE in the array, and initialize the image and the detail’s overview setting:

  CollapsiblePanelExtenders[ID] = { collapsed: !(expanded || false) };
  $("#" + ImageControlID).attr("src", (expanded || false) ? ExpandedImage : CollapsedImage);
  $("#" + TargetControlID).attr("overflow", "hidden");

Finally, we add a click handler on the header panel:

  $("#" + CollapseControlID).click(function (pa) {
    if (CollapsiblePanelExtenders[ID].collapsed) {
      $("#" + TargetControlID).css("height", "");
      $("#" + ImageControlID).attr("src", ExpandedImage);
      CollapsiblePanelExtenders[ID].collapsed = false;
    } else {
      $("#" + TargetControlID).css("height", "0px");
      $("#" + ImageControlID).attr("src", CollapsedImage);
      CollapsiblePanelExtenders[ID].collapsed = true;
    }
    if (fn) {
      fn(CollapsiblePanelExtenders[ID].collapsed);
    }
  });
};

The click handler adjusts the detail’s height and sets the expand/collapse image. If a callback function is defined, the function is executed with the collapsed status as parameter.

I was surprised that it only takes a couple of JavaScript lines to implement an ACT control. Note, however, that the presented solution does not invoke any animations – have a look at jQuery Effects, if you need them…


Adding MVC 3 to an existing ASP.Net 4 Web Application

March 30, 2014

If you maintain an ASP.Net application for some time (as I do ;) ), and also gained experience in developing using ASP.Net MVC, you probably want to implement any new features in MVC.

So I search for ways to add MVC to an existing ASP.Net application, and found a couple of solutions. However, they refer to older versions of MVC, so I’ll sketch here which steps are required for ASP.Net 4 and MVC 3.

The original information comes from these articles:

For some changes and additions, it’s easier if you already have an MVC project. If you do not, simply create a new “MVC3 Web Application” – all the required files will be generated.

Add assemblies

In your project, add references to the following assemblies:

  • System.Web.Abstractions
  • System.Web.Mvc
  • System.Web.Routing

Unload project and edit project

Under <ProjectTypeGuids>, add the Guid “{E53F8FEA-EAE0-44A6-8774-FFD645390401};” to get MVC-specific “Add…” items in the context menu of the Solution Explorer, such as Add Controller and Add View. The line then reads

 <ProjectTypeGuids>{E53F8FEA-EAE0-44A6-8774-FFD645390401};
    {349c5851-65df-11da-9384-00065b846f21};
    {fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>

In the first <PropertyGroup>, add the line

<MvcBuildViews>true</MvcBuildViews>

Add the end of the project file, add the MVC build target:

<Target Name="MvcBuildViews" AfterTargets="AfterBuild" Condition="'$(MvcBuildViews)'=='true'">
  <AspNetCompiler VirtualPath="temp" PhysicalPath="$(WebProjectOutputDir)" />
</Target>

Edit web.config

Under <system.web>/<compilation>, add the following assembly references

<assemblies>
  <add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
  <add assembly="System.Web.Helpers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
  <add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
  <add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
  <add assembly="System.Web.WebPages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
</assemblies>

Under <system.web>/<pages>, add the following namespace declarations

<namespaces>
  <add namespace="System.Web.Helpers" />
  <add namespace="System.Web.Mvc" />
  <add namespace="System.Web.Mvc.Ajax" />
  <add namespace="System.Web.Mvc.Html" />
  <add namespace="System.Web.Routing" />
  <add namespace="System.Web.WebPages"/>
</namespaces>

Probably you also need to tweak assembly bindings

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

Project additions

In the project, add the folders Controllers and Views, and Views/Shared.

Copy the Views/web.config file from the other MVC application.

Edit Global.asax.cs

Add these using statements:

using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

Add these lines to Application_Start():

AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);

If you want to use the MVC Razor engine, also add:

ViewEngines.Engines.Add(new RazorViewEngine());

Add the methods RegisterRoutes() and RegisterGlobalFilters() as:

protected static void RegisterRoutes(RouteCollection routes)
{
  routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
  routes.IgnoreRoute("{resource}.aspx/{*pathInfo}");
  routes.MapRoute("Default",
    "{controller}/{action}/{id}",
    new { controller = "Home", action = "Index", id = "" }
  );
}

protected static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
  filters.Add(new HandleErrorAttribute());
}

Personally, I prefer to have each action in a separate controller class, so I use the solution presented here:

routes.MapActionRoute(
  "Default",
  "{controller}/{action}/{oid}",
  new { controller = "Default", action = "Index", oid = UrlParameter.Optional },
  new object { }
);

Start Page

If you have a defined Start Page in the ASP.Net project, you will need to redirect there from the default MVC action:

namespace MyMvcWeb.Controllers
{
  public class DefaultController : Controller
  {
    public ActionResult Index()
    {
      return Redirect(Url.Content("~/default.aspx"));
    }
  }
}

Mixing Razor and WebForms views

Razor and WebForms are two different view engines, and cannot be mixed.*

If you want to use an ASP.Net master page for a WebForms MVC view, have a look at this SO answer.

As for WebForms, the ASP.Net master page derives from System.Web.UI.MasterPage, whereas an MVC master page derives from System.Web.Mvc.ViewMasterPage (which itself also derives from System.Web.UI.MasterPage).

As for Razor, master pages have been replaced by layout views. Both views and layout views derive from System.Web.Mvc.WebViewPage<T> which has no common base class with MasterPage.

*) Note that there is a solution to using Razor views in ASP.Net masterpages (Hanselman, Hawley, Baloch), but it looks a bit hacky and fragile – and, of course, unsupported ;)

Debugging Routes

If things do not turn out as expected after implementing all these changes, have a look at the Route Debugger (original post, updated version).


Using Linked Files in ASP.Net Applications

March 24, 2014

One of the web applications I maintain still runs on ASP.Net 3.5. A specific feature required that there also be a .Net 4 version of the application, so I thought about how to share the source code for both .Net versions.

Essentially, this can be achieved by creating a new solution and new projects and having the projects link to the original source files. That’s ok so far and it works fine.

However, when it comes to the ASP.Net application itself, things do not work straight forward:

My build process invokes both aspnet_compiler and aspnet_merge where aspnet_compiler requires all markup files to be present in the project directory, as it seems to ignore the contents of the .csproj file.

So how do I get the markup files into the linking project without manually copying the files? The process should be performed automatically, so that any changes in the original source are propagated to the linking project.

Luckily I found this article Using Linked Files in ASP.Net Applications, which unfortunately has been removed recently. In principle, you need to specify new build targets that invoke the file copy for the linked files. While the original solution only covered copying “content” files, I extended it to also copy “compile” files:

<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Target Name="CopyLinkedContentFiles">
    <Copy Condition=" '%(Content.Link)' != '' " 
                SourceFiles="%(Content.Identity)" 
                DestinationFiles="$(OutputPath)\..\%(Content.Link)" 
                SkipUnchangedFiles="true" />
    <Message Text="Copying linked file %(Content.Link)" />
  </Target>
  <Target Name="CopyLinkedCompileFiles">
    <Copy Condition=" '%(Compile.Link)' != '' " 
                SourceFiles="%(Compile.Identity)" 
                DestinationFiles="$(OutputPath)\..\%(Compile.Link)" 
                SkipUnchangedFiles="true" />
    <Message Text="Copying linked file %(Compile.Link)" />
  </Target>
  <PropertyGroup>
    <BuildDependsOn>
      CopyLinkedContentFiles;
      CopyLinkedCompileFiles;
      $(BuildDependsOn);
    </BuildDependsOn>
  </PropertyGroup>

Save the file as CopyLinks.targets in your project directory. Then edit the .csproj file to add the line

  <Import Project="$(MSBuildProjectDirectory)\CopyLinks.targets" />

(find other <Import> sections and insert there).

As you build your  project, the linked files will now be copied to the respective location in the project linked the original files.

The complete targets file can be downloaded here.


Follow

Get every new post delivered to your Inbox.

Join 65 other followers