Thursday, August 18, 2011

Getting Started with the F# PowerPack - Part 2

In part 1 of this series, I provided a few links and examples of how to get started with the modules and types found in the math directory of the F# PowerPack. In this post, I'll do something similar with the command-line argument parser, AsyncOperations, AsyncStreamReader, and AsyncWorker.

ArgParser:

The F# PowerPack comes with a command-line parser that makes it easy to parse command-line options provided by a user.

The following links provide examples:

Example by Laurent Le Brun
Example by Robert Pickering
C# Port of Laurent Le Brun's Example by Steve Gilham

AsyncOperations:

Within the AsyncOperations.fsi file you'll find type extensions for StreamReader (AsyncReadToEnd) and File (AsyncOpenText, AsyncOpenRead, AsyncOpenWrite, AsyncAppendText, and AsyncOpen).

The following example is a modified version of one of the PowerPack unit tests, which shows AsyncOpenWrite and AsyncOpenRead in use.
open System.IO

async {
    async {
        let buffer = "F# is fun!"B
        use! is = File.AsyncOpenWrite "test.txt"
        do! is.AsyncWrite(buffer, 0, buffer.Length) 
        printfn "File written" } 
    |> Async.RunSynchronously

    async { 
        let buffer = Array.zeroCreate<byte>(7)
        use! is = File.AsyncOpenRead "test.txt"
        let! count = is.AsyncRead(buffer, 0, 7)
        printfn "File read"
        return count, buffer }
    |> Async.RunSynchronously |> ignore
} 
|> Async.Start

AsyncStreamReader:

The AsyncStreamReader type provides a simple approach to reading streams asynchronously. Here's an example:
open System
open System.IO
open System.Text

let text = [ new String([|for i in 1..1022 do yield 'x'|])
             new String([|for i in 1..1022 do yield 'y'|])
             new String([|for i in 1..1022 do yield 'z'|]) ].ToString()  

use stream = new MemoryStream()
stream.Write(Encoding.UTF8.GetBytes(text), 0, text.Length)
stream.Position <- 0L
let reader = new AsyncStreamReader(stream, Encoding.UTF8)          
async { let rec readAllChars () =
            async {
                let! eof = reader.EndOfStream
                if not eof then
                    let! character = reader.Read()
                    do printf "%s" (character.ToString())
                    return! readAllChars ()
                else 
                    printfn "%sRead complete" Environment.NewLine           
            }
        do! readAllChars ()
} 
|> Async.Start
Another example of AsyncStreamReader can be found here (see the answer provided by Brian McNamara).

AsyncWorker:

The AsyncWorker type provides a great way to define and launch a background worker. Don Syme shows how to create and use a similar type in his Async and Parallel Design Patterns in F#: Reporting Progress with Events (plus Twitter Sample) post.

Here's an example of the AsyncWorker type from the PowerPack.
open Microsoft.FSharp.Control

let rec job = async {   
        for i in 1 .. 20 do  
            printfn "doing some work"
            do! Async.Sleep 300
            worker.ReportProgress i
    }
    and worker : AsyncWorker<_> = AsyncWorker(job)

worker.ProgressChanged.Add(fun jobNumber -> printfn "job %d completed" jobNumber) 
worker.Error.Add(fun err -> printfn "Error: %A" err.Message)
worker.Completed.Add(fun _ -> printfn "All jobs have completed")
worker.Canceled.Add(fun _ -> printfn "Jobs have been canceled")

worker.RunAsync() |> ignore
Wrapping Up:

You can find working examples of each of these on my GitHub https://github.com/dmohl/FSharpPowerPackExample. Also, if you're interested in seeing more async examples, check out the series (C# Async Examples in F#) that Chris Marinos just started.

Sunday, August 14, 2011

Another CoffeeScript and jQuery UI Example

In a previous post, I showed a few examples of jQuery UI demos ported to CoffeeScript. While I prefer the approach that was shown in that post, there is another way that I could have done it. In this post, I'll show a different jQuery UI example (one of the the Slider demos) written using the CoffeeScript class structure.

You can find the full example (F# + ASP.NET MVC + CoffeeScript + jQuery UI) on my GitHub: https://github.com/dmohl/FsCoffeeScriptjQueryUIExample.
class Slider 
    constructor: ->
        $('#master').slider 
            value: 60 
            orientation: "horizontal" 
            range: "min" 
            animate: true

        $('#eq > span').each -> 
            value = parseInt $(this).text(), 10
            $(this).empty().slider
                value: value
                range: "min"
                animate: true
                orientation: "vertical" 

window.slider = new Slider()

Thursday, August 11, 2011

Getting Started with the F# PowerPack

I've been talking quite a bit about the F# PowerPack NuGet packages (see this post and this post for examples); however, I haven't written anything about the specific features that the F# PowerPack contains. To remedy this, I plan to do a series of posts to explore or review some of these features. The F# PowerPack has a wide variety of aspects, so if the features mentioned in this post are not of interest to you, don't let that stop you from reading future posts.

Note: The latest version of the F# PowerPack does not have official documentation. Documentation for version 1.9.6.16 can be found at http://research.microsoft.com/en-us/um/cambridge/projects/fsharp/manual/fsharp.powerpack/Microsoft.FSharp.Math.html.

I'll begin by looking at some of the modules and types found in the FSharp.PowerPack\math directory of the F# PowerPack source.

Associations:

The first module that we find in this directory is called GlobalAssociations. To quote from the comments in the F# PowerPack code, "Associations are a way of associating dictionaries of
operations with given types at runtime. Associations are global to a .NET application domain. Once specified an association may not be deleted or modified." The following links provide examples of this module in use:

F# INumerics Interface, and Matrix class by Yin
Example on StackOverflow by Tomas Petricek

Complex:

The module found in Complex.fs provides support for complex numbers and includes members that allow you to easily work with those numbers. A few of the members include retrieval of the real as well as imaginary parts of the complex number, addition/subtraction/multiplication of complex numbers, etc.

Here is a simple example from the F# PowerPack CodePlex home page:

let c = complex 0.0 1.0 * complex 0.0 1.0 // result is -1r+0i

Matrix:

There are several examples of using the Matrix type out on the web. The F# PowerPack CodePlex home page provides the following example:

let v  = vector [1.0;1.0;1.0] + vector [2.0;2.0;2.0] // result is (3.0; 3.0; 3.0)

Additionally, the previously mentioned post by Yin provides a few good examples. Also, Chapter 10 of the book Expert F# by Don Syme, Adam Granicz, and Antonio Cisternino has a section called Introducing Microsoft.FSharp.Math which contains a little more information and a few simple examples. A more involved example can be found here.

NativeArrayExtensionsForMatrix:

This module adds a few extension methods (i.e. of_vector, of_rowvec, and of_matrix) to the PinnedArray and PinnedArray2 types. An example of of_vector in use can be found here.

BigRational:

A simple example of the BigRational type in use, as documented on the F# PowerPack CodePlex home page, is shown below.

let r = (1N/2N) * (1N/3N) // result is 1/6

A more complex example can be found here.

Wrapping Up:

That's all for now. Everything described in this post (and the items described in the next few posts for this series) is provided in the FSharp.PowerPack.dll, which is available as part of an MSI on CodePlex as well as on NuGet Gallery as package ID FSPowerPack.Core.Community. In the next post for this series I will walk through and point out or provide examples for the command-line argument parser, AsyncOperations, AsyncStreamReader, and AsyncWorker.

Sunday, August 7, 2011

New F# PowerPack Packages on NuGet Gallery

There are several new F# PowerPack NuGet packages now available on NuGet Gallery. Additionally, the original F# PowerPack NuGet package has been modified slightly. A description of each PowerPack related package is provided below:

FSPowerPack.Community:


This is the original package. It includes all of the F# PowerPack libraries, but no longer includes the example .fs file. FSPowerPack.Community.Sample should be used when the example .fs file is desired.

FSPowerPack.Community.Sample:


This matches the contents of the original package (i.e. version 2.1.0.1 and below). It includes all F# PowerPack libraries as well as a simple .fs example file.

FSPowerPack.Core.Community:


This package contains only the FSharp.PowerPack.dll files for the various targeted frameworks.

FSPowerPack.Linq.Community:


This package contains the FSharp.PowerPack.Linq.dll files for the various targeted frameworks. It also takes a dependency on the FSPowerPack.Core.Community package.

FSPowerPack.Metadata.Community:


This package contains the FSharp.PowerPack.Metadata.dll files for the various targeted frameworks. It also takes a dependency on the FSPowerPack.Core.Community package.

FSPowerPack.Parallel.Seq.Community:


This package contains the FSharp.PowerPack.Parallel.Seq.dll files. It also takes a dependency on the FSPowerPack.Core.Community package.

Sunday, July 31, 2011

jQuery UI Examples in CoffeeScript

Recently, I've been playing with CoffeeScript and have found it to be a lot of fun. I'm a big fan of JavaScript and CoffeeScript has only added to the enjoyment of writing client-side code by taking away a few of the "bad parts" and reducing syntactic noise. To help learn the language, I took a few of the jQuery UI examples and re-wrote them in CoffeeScript. You can find the full solution (F# + ASP.NET MVC (Razor) + CoffeeScript) at https://github.com/dmohl/FsCoffeeScriptjQueryUIExample. (Note: You will need to install the Mindscape Web Workbench Visual Studio 2010 extension to make the CoffeeScript aspects work correctly. Visit this post by Scott Hanselman for information on getting started with this extension).

Here are the before and after examples:

This simple Portlets example comes from the jQuery UI demo found at http://jqueryui.com/demos/sortable/#portlets.

The JavaScript Version:

(function (portlets, undefined) {
    portlets.init = function() {
        $(".column").sortable({
            connectWith: ".column"
        });

        $(".portlet")
	        .addClass("ui-widget ui-widget-content ui-helper-clearfix ui-corner-all")
	        .find(".portlet-header").addClass("ui-widget-header ui-corner-all")
	        .prepend("<span class='ui-icon ui-icon-minusthick'></span>")
	        .end().find(".portlet-content");

        $(".portlet-header .ui-icon").click(function () {
            $(this).toggleClass("ui-icon-minusthick").toggleClass("ui-icon-plusthick");
            $(this).parents(".portlet:first").find(".portlet-content").toggle();
        });

        $(".column").disableSelection();
    };

} (window.portlets = window.portlets || {}));

The CoffeeScript Version:

((portlets) ->
  portlets.init = ->  
    $(".column").sortable(connectWith: ".column").disableSelection()

    $(".portlet")
      .addClass("ui-widget ui-widget-content ui-helper-clearfix ui-corner-all")
      .find(".portlet-header").addClass("ui-widget-header ui-corner-all")
      .prepend("<span class='ui-icon ui-icon-minusthick'></span>")
      .end().find ".portlet-content"

    $(".portlet-header .ui-icon").click -> 
      $(this).toggleClass("ui-icon-minusthick").toggleClass "ui-icon-plusthick"
      $(this).parents(".portlet:first").find(".portlet-content").toggle()
) window.portlets = window.portlets or {}

This simple Photo Manager example comes from the jQuery UI demo found at  http://jqueryui.com/demos/droppable/#photo-manager.

The JavaScript Version:

(function (pictureManager, undefined) {
    pictureManager.init = function() {
        var $gallery = $("#gallery"),
		    $trash = $("#trash");

	    $( "li", $gallery ).draggable({
		    cancel: "a.ui-icon",
		    revert: "invalid", 
		    containment: $( "#demo-frame" ).length ? "#demo-frame" : "document", 
		    helper: "clone",
		    cursor: "move"
	    });

	    $trash.droppable({
		    accept: "#gallery > li",
		    activeClass: "ui-state-highlight",
		    drop: function( event, ui ) {
			    deleteImage( ui.draggable );
		    }
	    });

	    $gallery.droppable({
		    accept: "#trash li",
		    activeClass: "custom-state-active",
		    drop: function( event, ui ) {
			    recycleImage( ui.draggable );
		    }
	    });

        $("ul.gallery > li").click(function (event) {
            var $item = $(this), $target = $(event.target);
            if ($target.is("a.ui-icon-trash")) {
                deleteImage($item);
            } else if ($target.is("a.ui-icon-zoomin")) {
                viewLargerImage($target);
            } else if ($target.is("a.ui-icon-refresh")) {
                recycleImage($item);
            }
            return false;
        });

        var recycle_icon = "<a href='link/to/recycle/script/when/we/have/js/off' title='Recycle this image' class='ui-icon ui-icon-refresh'>Recycle image</a>";
        function deleteImage($item) {
            $item.fadeOut(function () {
                var $list = $("ul", $trash).length ?
				$("ul", $trash) :
				$("<ul class='gallery ui-helper-reset'/>").appendTo($trash);

                $item.find("a.ui-icon-trash").remove();
                $item.append(recycle_icon).appendTo($list).fadeIn(function () {
                    $item
					.animate({ width: "48px" })
					.find("img")
						.animate({ height: "36px" });
                });
            });
        }

        var trash_icon = "<a href='link/to/trash/script/when/we/have/js/off' title='Delete this image' class='ui-icon ui-icon-trash'>Delete image</a>";
        function recycleImage($item) {
            $item.fadeOut(function () {
                $item
				.find("a.ui-icon-refresh")
					.remove()
				.end()
				.css("width", "96px")
				.append(trash_icon)
				.find("img")
					.css("height", "72px")
				.end()
				.appendTo($gallery)
				.fadeIn();
            });
        }

        function viewLargerImage($link) {
            var src = $link.attr("href"),
			title = $link.siblings("img").attr("alt"),
			$modal = $("img[src$='" + src + "']");

            if ($modal.length) {
                $modal.dialog("open");
            } else {
                var img = $("<img alt='" + title + "' width='384' height='288' style='display: none; padding: 8px;' />")
				.attr("src", src).appendTo("body");
                setTimeout(function () {
                    img.dialog({
                        title: title,
                        width: 400,
                        modal: true
                    });
                }, 1);
            }
        }
    };
} (window.pictureManager = window.pictureManager || {}));

The CoffeeScript Version:

((pictureManager) ->
  pictureManager.init = ->
    $gallery = $("#gallery")
    $trash = $("#trash")
    recycle_icon = "<a href='link/to/recycle/script/when/we/have/js/off'  
                            title='Recycle this image' 
                            class='ui-icon ui-icon-refresh'>Recycle image</a>"
    trash_icon = "<a href='link/to/trash/script/when/we/have/js/off' 
                         title='Delete this image' 
                         class='ui-icon ui-icon-trash'>Delete image</a>"

    deleteImage = ($item) ->
      $item.fadeOut ->
        $list = if $("ul", $trash).length then $("ul", $trash) else $("<ul class='gallery ui-helper-reset'/>").appendTo $trash
        $item.find("a.ui-icon-trash").remove()
        $item.append(recycle_icon).appendTo($list).fadeIn ->
          $item.animate(width: "48px").find("img").animate height: "36px"

    recycleImage = ($item) ->
      $item.fadeOut ->
        $item.find("a.ui-icon-refresh").remove().end()
          .css("width", "96px").append(trash_icon).find("img")
          .css("height", "72px").end().appendTo($gallery).fadeIn()

    viewLargerImage = ($link) ->
      src = $link.attr "href"
      title = $link.siblings("img").attr "alt"
      $modal = $("img[src$='#{src}']")
      if $modal.length
        $modal.dialog "open"
      else
        img = $("<img alt='#{title}' width='384' height='288' 
               style='display: none; padding: 8px;' />")
               .attr("src", src).appendTo "body"
        setTimeout (->
          img.dialog 
            title: title
            width: 400
            modal: true
        ), 1

    $("li", $gallery).draggable 
      cancel: "a.ui-icon"
      revert: "invalid"
      containment: if $("#demo-frame").length then "#demo-frame" else "document"
      helper: "clone"
      cursor: "move"
    
    $trash.droppable 
      accept: "#gallery > li"
      activeClass: "ui-state-highlight"
      drop: (event, ui) ->
        deleteImage ui.draggable
    
    $gallery.droppable 
      accept: "#trash li"
      activeClass: "custom-state-active"
      drop: (event, ui) ->
        recycleImage ui.draggable
    
    $("ul.gallery > li").click (event) ->
      $item = $(this)
      $target = $(event.target)
      if $target.is "a.ui-icon-trash"
        deleteImage $item
      else if $target.is "a.ui-icon-zoomin"
        viewLargerImage $target
      else recycleImage $item  if $target.is "a.ui-icon-refresh"
      false    
) window.pictureManager = window.pictureManager or {}

This simple User Manager example comes from the jQuery UI demo found at  http://jqueryui.com/demos/dialog/#modal-form.

The JavaScript Version:

(function (userManager, undefined) {
    userManager.init = function () {
        $("#dialog:ui-dialog").dialog("destroy");

        var name = $("#name"),
			email = $("#email"),
			password = $("#password"),
			allFields = $([]).add(name).add(email).add(password),
			tips = $(".validateTips");

        function updateTips(t) {
            tips
				.text(t)
				.addClass("ui-state-highlight");
            setTimeout(function () {
                tips.removeClass("ui-state-highlight", 1500);
            }, 500);
        }

        function checkLength(o, n, min, max) {
            if (o.val().length > max || o.val().length < min) {
                o.addClass("ui-state-error");
                updateTips("Length of " + n + " must be between " +
					min + " and " + max + ".");
                return false;
            } else {
                return true;
            }
        }

        function checkRegexp(o, regexp, n) {
            if (!(regexp.test(o.val()))) {
                o.addClass("ui-state-error");
                updateTips(n);
                return false;
            } else {
                return true;
            }
        }

        $("#dialog-form").dialog({
            autoOpen: false,
            height: 300,
            width: 350,
            modal: true,
            buttons: {
                "Create an account": function () {
                    var bValid = true;
                    allFields.removeClass("ui-state-error");

                    bValid = bValid && checkLength(name, "username", 3, 16);
                    bValid = bValid && checkLength(email, "email", 6, 80);
                    bValid = bValid && checkLength(password, "password", 5, 16);

                    bValid = bValid && checkRegexp(name, /^[a-z]([0-9a-z_])+$/i, "Username may consist of a-z, 0-9, underscores, begin with a letter.");
                    bValid = bValid && checkRegexp(email, /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i, "eg. ui@jquery.com");
                    bValid = bValid && checkRegexp(password, /^([0-9a-zA-Z])+$/, "Password field only allow : a-z 0-9");

                    if (bValid) {
                        $("#users tbody").append("<tr>" +
							"<td>" + name.val() + "</td>" +
							"<td>" + email.val() + "</td>" +
							"<td>" + password.val() + "</td>" +
						"</tr>");
                        $(this).dialog("close");
                    }
                },
                Cancel: function () {
                    $(this).dialog("close");
                }
            },
            close: function () {
                allFields.val("").removeClass("ui-state-error");
            }
        });

        $("#create-user")
			.button()
			.click(function () {
			    $("#dialog-form").dialog("open");
			});        
    };
} (window.userManager = window.userManager || {}));

The CoffeeScript Version:

((userManager) ->
  userManager.init = ->
    name = $("#name")
    email = $("#email")
    password = $("#password")
    allFields = $([]).add(name).add(email).add password
    tips = $(".validateTips")

    updateTips = (t) ->
      tips.text(t).addClass "ui-state-highlight"
      setTimeout (->
        tips.removeClass "ui-state-highlight", 1500
      ), 500

    checkLength = (o, n, min, max) ->
      if o.val().length > max or o.val().length < min
        o.addClass "ui-state-error"
        updateTips "Length of #{n} must be between #{min} and #{max}."
        false
      else true

    checkRegexp = (o, regexp, n) ->
      unless regexp.test o.val()
        o.addClass "ui-state-error"
        updateTips n
        false
      else true

    $("#dialog-form").dialog 
      autoOpen: false
      height: 300
      width: 350
      modal: true
      buttons: 
        "Create an account": ->
          bValid = true
          allFields.removeClass "ui-state-error"
          bValid = bValid and checkLength name, "username", 3, 16
          bValid = bValid and checkLength email, "email", 6, 80
          bValid = bValid and checkLength password, "password", 5, 16
          bValid = bValid and checkRegexp name, /^[a-z]([0-9a-z_])+$/i, "Username may consist of a-z, 0-9, underscores, begin with a letter."
          bValid = bValid and checkRegexp email, /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i, "eg. ui@jquery.com"
          bValid = bValid and checkRegexp password, /^([0-9a-zA-Z])+$/, "Password field only allow : a-z 0-9"
          if bValid
            $("#users tbody").append "<tr>" + "<td>" + name.val() + "</td>" + "<td>" + email.val() + "</td>" + "<td>" + password.val() + "</td>" + "</tr>"
            $(this).dialog "close"
        
        Cancel: ->
          $(this).dialog "close"
      
      close: ->
        allFields.val("").removeClass "ui-state-error"
    
    $("#dialog:ui-dialog").dialog "destroy"

    $("#create-user").button().click ->
      $("#dialog-form").dialog "open"
) window.userManager = window.userManager or {}

Sunday, July 17, 2011

WPF Behaviors in the F# WPF Project Templates

In my last post, I mentioned that I was working on a few enhancements to the my F# WPF Project Templates that are out on Visual Studio Gallery. I'm happy to say that the new versions of the F# and C# Windows App (WPF, MVVM) and F# Windows App (WPF, MVVM) templates are now available.

What Was Changed?

For the F# Windows App (WPF, MVVM) template, a few WPF behaviors have been added. I'll talk about these changes in a bit (for those that can't wait, see http://fssnip.net/62 and http://fssnip.net/6h).

The F# and C# Windows App (WPF, MVVM) template includes all of the same changes as the F# only template. Additionally, I've taken the multiple F# projects that were previously provided in this template and coalesced them into one F# project. I believe that this change better reflects the real-world use of this template.

You Mentioned WPF Behaviors?

The biggest change to the templates is the addition of examples of a few different types of WPF behaviors. These templates include examples of event to command as well as a type converter. The event to command implementation is Blendable (meaning that it plays well with Expression Blend) and is loosely based on the MVVM Light Toolkit EventToCommand implementation.

Here's the code:

namespace FSharpWpfMvvmTemplate.Behavior

open System
open System.Windows
open System.Windows.Input
open System.Windows.Interactivity

type EventToCommand() =
    inherit TriggerAction<DependencyObject>()
    [<DefaultValue(false)>] 
    static val mutable private CommandProperty:DependencyProperty
    [<DefaultValue(false)>] 
    static val mutable private CommandParameterProperty:DependencyProperty
    
    /// Set the command dependency property
    static do 
        EventToCommand.CommandProperty <-
            DependencyProperty.Register("Command", 
                typeof<ICommand>, typeof<EventToCommand>)
    
    /// Set the command parameter dependency property
    static do 
        EventToCommand.CommandParameterProperty <-
            DependencyProperty.Register("CommandParameter", 
                typeof<obj>, typeof<EventToCommand>)
    
    /// Get/Set the Command 
    member this.Command 
        with get() = this.GetValue EventToCommand.CommandProperty :?> ICommand
        and set value = this.SetValue(EventToCommand.CommandProperty, value)
    
    /// Get/Set the CommandParameter 
    member this.CommandParameter 
        with get() = this.GetValue EventToCommand.CommandParameterProperty 
        and set value = this.SetValue(EventToCommand.CommandParameterProperty, value)
    
    /// Implement the Invoke method from TriggerAction to execute the command
    override this.Invoke parameter = 
        let command = this.Command
        let commandParameter = match this.CommandParameter with
                               | null -> parameter
                               | commandParam -> commandParam  
        if command <> null && command.CanExecute(commandParameter) then
            command.Execute(commandParameter)

Here's the XAML that uses this code:

        <Button Grid.Row="2" Command="{Binding ApproveExpenseReportCommand}" 
                Style="{StaticResource buttonStyle}" Content="Approve"
                Grid.Column="1" Margin="0,10,53,0">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="MouseEnter">
                    <Behavior:EventToCommand 
                            Command="{Binding MouseEnterButtonCommand}" 
                            CommandParameter="Approve" />
                </i:EventTrigger>
                <i:EventTrigger EventName="MouseLeave">
                    <Behavior:EventToCommand 
                            Command="{Binding MouseLeaveButtonCommand}" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>

The value converter example uses a ConverterBase class that simplifies the creation of any concrete implementations. This base class uses a combination of functional and object-oriented programming to provide a default implementation of Convert and ConvertBack. The child class can then provide a function to override the implementation of either or both of these methods. For the sake of brevity, I will not post the ConvertBase code here, but you can go to http://fssnip.net/62 to see it.

The concrete implementation that is provided as an example in these project templates is shown below:

namespace FSharpWpfMvvmTemplate.Behavior

open System
open System.Windows
open System.Windows.Data
open System.Windows.Media
open ConverterBase

/// Returns Visibility.Visible if the string is not null or empty
type StringExistsToVisibilityConverter() =
    inherit ConverterBase()
    let convertFunc = fun (v:obj) _ _ _ ->         
        match String.IsNullOrEmpty(string v) with
        | false -> Visibility.Visible
        | _ -> Visibility.Collapsed
        :> obj
    override this.Convert = convertFunc 

This can be used in XAML like this:

<TextBlock Grid.Row="3" HorizontalAlignment="Center" Margin="0,10,0,0" 
                FontSize="12" FontWeight="Bold" 
                Grid.ColumnSpan="2" Text="{Binding LastApprovalDisplayMessage}" 
                Visibility="{Binding Path=LastApprovalDisplayMessage, 
                            Converter={StaticResource StringExistsToVisibility}}" />

While I don't have an example in these templates of an Attached Property written in F#, it is worth noting that you can find just such an example at http://fssnip.net/69.

Tuesday, July 5, 2011

A Few Items of Note

In this post, I'm going to highlight a few things that I have been working on that aren't big enough to warrant their own blog entry, but that are worth pointing out. Note: If you follow me on Twitter (i.e. @dmohl) you may have already heard about these things.

1. In Feb. of this year, I blogged about the F# PowerPack being added to NuGet gallery. Since that time, there have been several requests to add the .NET 4 binaries to that NuGet package. I'm happy to say that a new version of the FSPowerPack.Community package has been uploaded that includes these binaries.

2. In Jan. of this year, an F# ASP.NET MVC 3 template was announced. There have been a few reports of an exception on some operating systems during the extension installation that points to a specific path that is too long. For those of you who are experiencing this error, there is a VSIX with a reduced path length available for download at https://sites.google.com/site/danodocs/Home/FSMVC3_min.vsix.

3. Have you visited the F# Snippets site yet? If not, I strongly recommend checking it out. While you are there, have a look at the WPF/Silverlight Value Converter base class and sample concrete implementation as well as an example of a WPF/Silverlight Attached Property. I'd love to hear what you think. Along these lines, I've been working on a few additions to the F# WPF templates that have been announced on this blog. If you have any enhancement requests, send them my way.