January 29, 2008

A quick and dirty way to know current locale's decimal separator

Those who work with locales, where decimal separator is not a "." might have ran into issues doing calculations in script. From what I've seen, developers usually hard-code replacements for their local decimal separator to be substituted with a ".", which is expected by the script.

Let's assume you are parsing an XML file and you need to do some calculations:

   1:  'Let's assume your locale's decimal separator is a comma
   2:  strSomeValue = "3.1415" 'the value retrived from xml
   3:  'Now let's assume you need to multiply it by 2
   4:  result = strSomeValue * 2 'result is 62830

The problem is that script does not recognize "." as a decimal separator in a string if your locale's separator is different. Certainly, you can replace "." in your strings with your decimal separator (e.g. ","), but what if you don't know the decimal separator at design time? A quick and easy way to figure out what the current locale's decimal separator is: divide 1 by 2 (we know that if we convert this division result to string, a decimal separator would be the second character). Here's a handy function that can be used in scripts:

   1:  Public Function GetDouble(strNumeric)
   2:      strDecSep = Mid(CStr(1/2),2,1)
   3:      strNumeric = Replace(strNumeric, strDecSep, ".")
   4:      If IsNumeric(strNumeric) Then 
   5:          GetDouble = CDbl(strNumeric)
   6:      Else
   7:          GetDouble = 0.00
   8:      End If
   9:  End Function

January 10, 2008

Switchvox new voicemail email -> SMS notification

Switchvox offers a wonderful email notification feature for new voicemail messages, but what if you are not at your computer? Naturally, the first thing that comes to mind is to forward those email notifications to your cell-phone via SMS.


Email notification sent by the PBX looks approximately like the one below:

From: SwitchvoxPBX
Sent: Thursday, January 10, 2008 2:42 PM
To: John Doe
Subject: New message 8 in mailbox 102

Dear John Doe:

Just wanted to let you know you just received a 0:02 long message (number 8) in mailbox 102 from "MARY JANE" <18001234567>, on Thursday, January 10, 2008 at 02:42:04 PM so you might want to check it when you get a chance.

You can check your message via the phone or use the links below to download your message:

Use the link below to just download the voicemail.
*Note* This does not mark it as read in the system.
https://SwitchvoxPBX/main?cmd=play_voicemail&msgnum=1

Use this link below to download the voicemail and mark it as read.
*Note* This will move this voicemail message to your Old folder and this link will no longer work.
https://SwitchvoxPBX/main?cmd=play_voicemail&msgnum=1&mark=read

Use this link below to download the voicemail and delete it.
*Note* This will completely remove the voicemail from the system and this link will no longer work.
https://SwitchvoxPBX/main?cmd=play_voicemail&msgnum=1&mark=delete

Thanks!

The quick and dirty way to forward this to your cell-phone is by using Outlook’s rules: this would simply forward the message to your cell-phone’s email address (most wireless providers offer this feature). However, all you will get is most likely part of the message that ends with words “… received message (number 8) in mailbox”, because SMS message has a limit on the number of characters (sender’s email, subject will also be included and will eat up characters. Isn’t it frustrating? You receive SMS notification about new voicemail, but you don’t know who called, when, and if they actually left a message, or hung up realizing you are not there, because the message got trimmed due to character limit.

If you look at the Outlook’s rule actions, you might notice “run a script” option, which is triggered when rule conditions are true, and this is exactly what I need: grab a message, take only the information I need and send it to my cell-phone.

So, launch your Outlook if it is not already running and hit Alt-F11 or go to Tools > Macro > Visual Basic Editor and paste the following script:

   1:  Public Sub ForwardSMS(Item As MailItem)
   2:      ' Do not run during business hours
   3:       If Time() < #8:00:00 AM# Or Time() > #5:00:00 PM# Then
   4:      ' Check to make sure it is an Outlook mail message, otherwise
   5:      ' subsequent code will probably fail depending on what type
   6:      ' of item it is.
   7:      If TypeName(Item) = "MailItem" Then
   8:          Dim msgBody As String : msgBody = Item.Body
   9:          Dim oRegExp, oMatches, oMatch, strDuration, strMsgNumber, strMsgInfo, strResult
  10:          strResult = ""
  11:          Set oRegExp = CreateObject("VBScript.RegExp")
  12:          oRegExp.Pattern = ".*\n\s*.*you just received a (.*) long message \(number ([\d:]*)\) in mailbox \d\d\d from (.*) so you might want to check it.*"
  13:          Set oMatches = oRegExp.Execute(msgBody)
  14:          For Each oMatch In oMatches
  15:          If oMatch.SubMatches.Count = 3 Then
  16:                  strDuration = oMatch.SubMatches(0)
  17:                  strMsgNumber = oMatch.SubMatches(1)
  18:                  strMsgInfo = oMatch.SubMatches(2)
  19:              strResult = strResult & "Message number " & strMsgNumber & " (" & strDuration & ") received from " & strMsgInfo & VbCrLf
  20:          Else
  21:              strResult = strResult & oMatch.Value & vbCrLf
  22:          End If
  23:          Next
  24:          Set oMatches = Nothing
  25:          Set oRegExp = Nothing
  26:          
  27:          Dim oEmail As Object
  28:          Set oEmail = Application.CreateItem(olMailItem)
  29:          oEmail.Body = strResult
  30:          oEmail.Recipients.Add "1234567@my_wireless_provider.com"
  31:          oEmail.Send
  32:          Set oEmail = Nothing
  33:        End If
  34:        End If
  35:  End Sub

Customize if necessary. Few notes:

  1. Line 3 checks for business hours (so that script runs only when you are away from the computer). In case you want the script to always do its job, then remove line 3 and the corresponding line 34.
  2. Line 12 has a regular expression to get only required information from notification email's body. Change the expression if your notification text is different from the one displayed at the beginning of this post.
  3. Line 30 specifies email address of your cell-phone. You can also create a contact in Outlook (with the needed email) and instead of specifying email address in the script, you can specify contact's name here.

Close the Visual Basic Editor. Then go to Tools > Rules and Alerts in your Outlook to create a rule, that would take notification messages from your Switchvox PBX and then select “run a script” for the rule’s action. When you click on the word ‘script’, a dialog box should pop up, showing the list of scripts. Select ForwardSMS (script’s name is specified in line 1 in the code above). Please note, that this is a client-side rule. This means that Outlook must be running for the rule and script to work!

Now, when you receive your next notification email, the rule you created should trigger the script. As a result, you will receive SMS message on your cell-phone which would look like this:

johndoe@domain.com Message number 8 (0:02) received from "MARY JANE" <18001234567>, on Thursday, January 10, 2008 at 02:42:04 PM

Now, when you get such SMS it is clear who called, when, and that the person didn't actually leave a message (very short duration time is a hint).

Hope someone will find this useful :-)

UPDATE: As it was pointed out in the first comment to this post, Switchvox SMB allows you to achieve the described behavior (custom notifications) via bult-in functionality. However, the one used in our office is a Switchvox SOHO, which apparently does not offer the Custom Voicemail Message Notification.

May 09, 2007

The never ending XP installation...

Recently I've been asked by a friend to reinstall Windows XP on a computer that crashed for some reason. First, I ensured that the existing XP doesn't boot (also booted from some service CDs to check the hard drive and ran an antivirus) to arrive to a conclusion that reinstalling was the only option. The rest of the procedure was clear and performed not once: insert an original installation CD, boot from it, choose recovery mode for existing XP installation. Everything seemed to work fine until the 34th minute, when the install froze...

Reset, repeat the procedure — same freeze. Hmm... The computer is pretty new with an Intel 945 chipset, Intel Core 2 Duo processor and 1 Gb or RAM. Moreover, XP worked fine on it previously and no hardware changes were made since purchase. So I went to BIOS to disable all the fancy motherboard features and other options which are not needed during install (COM/LPT ports, on-board audio, USB, etc.), but this didn't help either. Removing/replacing a video adapter (which was basically the only 'extra' component plugged into the motherboard) was not an option (don't ask why).

I was really puzzled, so I decided to google if anybody else ran into similar trouble. Luckily, among a bunch of useless forum postings I found the following blog post: XP Installation - The 34 minute hiccup with additional discussion. Certainly, I didn't like the idea of messing with the *.inf files, but these resources also mentioned MSKB 828267. It became obvious that the problem is somewhere in the hardware.

The first thing I wanted to check if the install actually freezes at the 34th minute and here the tip about SHIFT+F10 was simply invaluable. Letting the install "hang" I periodically checked modification time and contents of the following files located in the %windir% folder:

  • setupapi.log
  • setupact.log
  • setuperr.log
  • pnplog.txt
and I saw that these files are being populated/updated (sic!), though it seemed that install froze.

Logs didn't show any errors during the time I monitored their contents (for about 4+ hours!), but at least I knew that the install didn't freeze and was actually doing something, so I just let it do whatever it did.

When I checked progress the next day when I came to the office (the install was working for the whole night) I was happy to see that it moved beyond the evil "34 minutes" and was asking for regional settings, etc. The rest of the procedure went faster, though not as fast as I expected, and finally it finished! XP booted fine, the Device manager didn't show any yellow exclamation marks — done!

This was the longest XP installation in my life. I don't know the actual reason why could it possibly take that long on such a powerful computer, but I was happy to get rid of it. Many thanks to techtracer's blog for the SHIFT+F10 tip!

February 12, 2007

CascadingDropDown: passing additional info and adding own events

Ajax.NET 1.0 came out of beta and luckily a project came up, so I had a good opportunity to try it out in real-world. This was my first experiment with Ajax.NET (formerly Atlas). Specifically, my point of interest was to Toolkit's CascadingDropDown to implement the required functionality. Samples looked very attractive and it seemed that it ideally fits my requirements until I actually started implementing the project...


To make things clearer for those reading this post, here is a brief description of what I was trying to achieve:

  • Create a web-form with a bunch of dropdowns allowing a visitor to specify computer (PC) hardware configuration (i.e. chassis, motherboard, processor, cooler, RAM, monitor, etc.), get the price, and place an order for it.
    • Configuration's price should only be displayed when all components required for a minimal configuration are selected (this was the client's requirement to prevent users from looking up individual prices for video-adapters, coolers and 'other things hidden in the box' easily. Naturally, they can calculate it anyways by picking different configurations, but this is neglectable).
      • UI should indicate by dropdown color which components are required for a minimal configuration.
    • Large components selected by the user should additionally be visualized, i.e. UI should display images for selected chassis, monitor, printer, scanner, speakers, keyboard, mouse.

Once again, the AJAX.NET Toolkit's CascadingDropDown extender seemed to be a perfect fit for the main part of the functionality. Adding an UpdatePanel with image placeholders (for component's image display) and a couple of labels to indicate which required components are not selected yet or to show the price of selected configuration, a ModalPopup for fancy order-placement (specifying client's name, email, and address of delivery) seemed to be sufficient to implement the web-form.

Implementation

The CascadingDropDown sample shows how to implement a simple hierarchy dependency. However, with computer hardware configuration it is a bit different (and a bit more complex):

  • motherboards have some dependency on chassis
  • processors depend on selected motherboard
  • coolers depend on processors (cooler is required only for tray processors and cannot be installed on box processors)
  • RAM depends on motherboard
  • HDD depends on motherboard
  • other components have no dependencies (e.g. DVD/CD, monitor, printer, scanner, keyboard, mouse, modem, speakers, UPS, software and the like)

Considering that the data for this project has to be updated frequently, and moreover, client's requirement was to feed the data from MS Excel, I decided to use a database for storage (MS Access mdb-file in particular to reduce hosting fees). Unfortunately, ASP.NET hosting is quite expensive when compared to *NIX hosting, and even more expensive when there is a need for MS SQL. Certainly, I could have used XML as well, but I thought it would make things a bit slow in terms of performance.

Passing additional information with Cascading drop down

I will not dwell upon the object model and class implementation, but will briefly describe what I came up with: each component with complex dependencies has additional compatibility keys (e.g. motherboard instance has the following fields: Name, Price, ChassisCompatibilityKey, ProcessorCompabilityKey, RAMCompatibilityKey, VideoCompatibilityKey, HDDCompatibilityKey).

In order to reduce the number of database requests I thought it would be a good idea to pass some additional information, besides the currently selected dropdown value, to the webserivce handling Ajax calls (as the original sample and probably the extender's implementation itself suggests). One ASP.NET forums member also asked for a solution on how to pass additional info in CascadingDropDowns, and basically, this is the only reason why I wrote this section. My solution is pretty straight-forward and very simple: why not concatenate the dropdown option's value? For instance, motherboard option's value would be: 99.99;~;2;~;1;~;2;~;3;~;4 (price and compatibility keys to indicate chassis/processor/ram/etc. type). I chose ;~; as a delimeter to ensure that I don't touch any data, and it is very unlikely that data would contain something weird like ;~; :-)

Thus, a dropdown's OnChange client-side event fires Ajax call and the web-service parses its value string to get required compatibility keys needed to populate other dropdowns that depend on this one. When the minimal configuration is selected, its price should be displayed, which is also very simple in this scenario: get selected value of each dropdown, split it by delimiter and add up the first item to configuration's price.

Adding own events to CascadingDropDown extender

One of the additional requirements was to have some UI indication of components which are required for minimal PC-configuration, e.g. different color of dropdown's PromptText. Unfortunately, the extender does not have a property like PromptTextCssClass or similar.

The first idea that came to my mind was to 'recolor' (change CSS class) of the prompt text client-side: loop through all dropdowns and change options[0].className to some defined CSS class. However, this approach yields no result for one simple reason: when the web-form is loaded, dropdowns do not have any options until all webservice calls are completed. Additional challenge would be to handle OnChange events for further 'recoloring'/re-styling of dependent dropdowns.

Thus the only solution is to re-style dropdown's options only AFTER the webservice returns results and the dropdown is populated. But how do we know when this happens? Regrettably, the CascadingDropDown extender does not have a OnClientPopulated event to inform subscribers that a particular dropdown finished populating its options.

Here's how I implemented the OnClientPopulated event in the CascadingDropDown extender:

1. Open the AjaxControlToolkit VS solution.

2. Modify the CascadingDropDownBehavior.js (add the following functions: add_populated, remove_populated, raisePopulated):

add_populated : function(handler) {
/// <summary>
/// Add a handler on the populated event
/// </summary>
/// <param name="handler" type="Function">
/// Handler
/// </param>
this.get_events().addHandler("populated", handler);
},

remove_populated : function(handler) {
/// <summary>
/// Remove a handler from the populated event
/// </summary>
/// <param name="handler" type="Function">
/// Handler
/// </param>
this.get_events().removeHandler("populated", handler);
},

raisePopulated : function(arg) {
/// <summary>
/// Raise the populated event
/// </summary>
/// <param name="arg" type="Sys.EventArgs">
/// Event arguments
/// </param>
var handler = this.get_events().getHandler("populated");
if (handler) handler(this.get_element(), arg Sys.EventArgs.Empty);
}

3. In CascadingDropDownBehavior.js locate _onMethodComplete function and modify it to look as follows (add line #4):

1:  _onMethodComplete : function(result, userContext, methodName) {
2:   // Success, update the DropDownList
3:   this._setOptions(result);
4:   this.raisePopulated(null);
5:  }

4. Open the CascadingDropDownExtender.cs and add the OnClientPopulatedBehavior:


[DefaultValue("")]
[Category("Behavior")]
[ExtenderControlEvent]
[ClientPropertyName("populated")]
public string OnClientPopulated
{
get
{
return (string)(ViewState["OnClientPopulated"] ?? string.Empty);
}
set { ViewState["OnClientPopulated"] = value; }
}

5. Build the solution.

6. Ensure that the project/solution you work on references this new version of the Toolkit!!!

7. Now in your aspx file you can add and set the OnClientPopulated property in your CascadingDropDown extender (intellisense should also work for this property now).

That's it :-) Hope this blog entry helps someone.