Thursday, February 26, 2009

F# Silverlight 2 and Isolated Storage

I recently picked up a copy of the book Silverlight 2 Recipes and wanted to try out some of the recipes by building F# version of them.   One of the first recipes that caught my eye was recipe 2.6 which talks about working with IsolatedStorage.  One of the main issues I had with implementing this recipe in F# is the fact that the public methods for IsolatedStorageFile is different between .NET Framework 3.5 and for Silverlight 2.  Therefore, I was unable to call the CreateFile method of IsolatedStorageFile .  But thankfully, this problem was already resolved by Michael Giagnocavo in his blog on F# 1.9.6.2 and Silverlight 2.  All I had to do is goto Project->[ProjectName] Properties –> Build and enter the following flag in “Other flags:”

--cliroot "C:\Program Files\Microsoft Silverlight\2.0.31005.0"

After configuring the ––cliroot flag, the source file compiled without problems.  Here’s the F# implementation of recipe 2.6 from the book:


#light
namespace SilverLightFSharp

open System.IO
open System.IO.IsolatedStorage
open System.Windows
open System.Windows.Controls
open System.Windows.Media
open System.ServiceModel
open List

// Recipe 2-6 from Silverlight 2 Recipes book
// Persisting Data on the Client

type MyPage() = class
inherit UserControl()

let settings = IsolatedStorageSettings.ApplicationSettings
let setting = "MySettings"
let filename = "FormFields.data"
let dataDirectory = "FormData"
let filepath = System.IO.Path.Combine(dataDirectory, filename)

//new () as this = {} then
do
let buildgrid color =
let grid = new Grid(Background = new SolidColorBrush(color))
let star = GridUnitType.Star

// Add column definitions
[0.06;0.455;0.485]
|> map (fun w -> new ColumnDefinition(Width=new GridLength(w,star)))
|> iter grid.ColumnDefinitions.Add

// Add row definitions
[0.08;0.217;0.61;0.093]
|> map (fun h -> new RowDefinition(Height=new GridLength(h,star)))
|> iter grid.RowDefinitions.Add

grid

let grid = buildgrid Colors.Gray

let gridadd (ui:#UIElement) row col =
grid.Children.Add(ui)
Grid.SetRow(ui,row)
Grid.SetColumn(ui,col)



let sp = new StackPanel(HorizontalAlignment = HorizontalAlignment.Stretch,
Margin=new Thickness(8.0,8.0,10.0,8.0))


new TextBlock(Text="Enter Setting Value",
TextWrapping=TextWrapping.Wrap,
Margin=new Thickness(4.0,4.0,4.0,4.0))
|> sp.Children.Add

let textdata = new TextBox(Height=126.0,
Text="",
TextWrapping=TextWrapping.Wrap,
Margin=new Thickness(4.0,4.0,4.0,4.0))
textdata |> sp.Children.Add

gridadd sp 2 1


// Build Form
let buildform () =
let brush = new LinearGradientBrush
(StartPoint = new Point(0.439999997615814,0.996999979019165),
EndPoint = new Point(0.560000002384186,0.00300000002607703))

[((255uy,58uy,108uy,87uy),0.0);
((255uy,163uy,189uy,163uy),0.536);
((255uy,58uy,108uy,87uy),0.968999981880188)]
|> map (fun (color,offset) -> new GradientStop(Color=(color |> Color.FromArgb), Offset=offset))
|> iter brush.GradientStops.Add

let form = new StackPanel(HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
Margin = new Thickness(4.0,4.0,4.0,4.0))
let border = new Border
(HorizontalAlignment = HorizontalAlignment.Stretch,
Margin = new Thickness(8.0,8.0,10.0,8.0),
VerticalAlignment = VerticalAlignment.Stretch,
CornerRadius = new CornerRadius(10.0,10.0,10.0,10.0),
Child=form,
Background=brush)
gridadd border 2 2
Grid.SetRowSpan(border,2)

let createLabel label = new TextBlock(Text=label,
TextWrapping=TextWrapping.Wrap,
Margin = new Thickness(2.0,4.0,2.0,0.0))
let createTextbox () = new TextBox(TextWrapping=TextWrapping.Wrap,
Margin = new Thickness(2.0,0.0,2.0,2.0))


let formelements =
["First Name:";"Last Name:"; "Company:"; "Title:"]
|> map (fun label -> (createLabel label, createTextbox ()))


formelements |> iter (fun (label,tb) ->
form.Children.Add(label)
form.Children.Add(tb))

formelements


let createbtn thickness row col content fn =

let btn = new Button(HorizontalAlignment = HorizontalAlignment.Stretch,
Margin=thickness,
Content=content)
btn.Click.Add(fn)
btn.Click.Add(fun evt -> ())
gridadd btn row col


let form = buildform()
let fields = form |> map (fun (label,tb) -> tb)

// SaveFormData_Click
createbtn (new Thickness(8.0)) 1 1 "Save Form Data"
<| (fun evt ->
try
use store = IsolatedStorageFile.GetUserStoreForApplication()
store.CreateDirectory(dataDirectory)
use stream = store.CreateFile(filepath)
use writer = new StreamWriter(stream)
writer.AutoFlush <- true
let data = fields |> map (fun field -> field.Text + "|")
|> fold_left (+) ""
data |> writer.WriteLine
textdata.Text <- "Data saved to : " + filepath + "\n" + data
with
| :? IsolatedStorageException as ex -> textdata.Text <- "Error saving setting: " + ex.Message
| _ -> textdata.Text <- "Unable to open file... ")

// ReadFormData_Click
createbtn (new Thickness(8.0)) 1 2 "Load Form Data"
<| (fun evt ->
try
use store = IsolatedStorageFile.GetUserStoreForApplication()
use file = new IsolatedStorageFileStream(filepath,FileMode.Open, store)
use reader = new StreamReader(file)
let storedvalues = reader.ReadToEnd();
let data = storedvalues.Split([|'|'|])
|> Array.to_seq |> Seq.take (List.length fields)
|> Seq.to_list
zip fields data |> iter (fun (tb,data) -> tb.Text <- data)
// For checking...
textdata.Text <- storedvalues
with
| :? IsolatedStorageException as ex -> textdata.Text <- "Error saving setting: " + ex.Message
| _ -> textdata.Text <- "Unable to open file... ")

// ButtonUpdateSetting - repurposed for diagnostics
createbtn (new Thickness(4.0,4.0,14.0,4.0)) 3 1 "Update Setting"
<| (fun evt ->
use store = IsolatedStorageFile.GetUserStoreForApplication()
let dirs = store.GetDirectoryNames("*")
let files = store.GetFileNames("*")
let dirtext = "Directories = " + (dirs |> Array.to_list |> map (fun t -> "\n"+t) |>fold_left (+) "")
let filetext = "Filenames = " + (files |> Array.to_list |> map (fun t -> "\n"+t) |>fold_left (+) "")
textdata.Text <- dirtext + "\n" + filetext)

base.Width <- 400.0
base.Height <- 300.0
base.Content <- grid

// Loaded...
try
if settings.Keys.Count <> 0 then
textdata.Text <- settings.[setting].ToString()
with _ as ex -> textdata.Text <- "Error saving setting: " + ex.Message

end

type MyApp = class
inherit Application

new () as this = {} then
this.Startup.Add(fun _ -> this.RootVisual <- new MyPage())
//base.Exit.Add( fun _ -> ()) //this.Application_Exit)
//this.InitializeComponent()
end

Wednesday, February 18, 2009

Interoperability – F# + Silverlight 2 on Apache + Metro Web Services on Tomcat

I bought the books Silverlight 2 in Action and Pro Silverlight 2 in C# 2008 for a while now and did not have a chance to work with Silverlight for the past several weeks.  However, recently I had to investigate using GlassFish’s Metro library to build web services as it was highly recommended by colleagues.  I have previous familiarity with using Apache Axis to build web services as that web services stack had strong plugin support with Eclipse, my IDE of choice when it comes to Java development.  I thought this was a great opportunity to build an sample web service using Metro and build the web service client on Silverlight platform.

My sample web service code was intentionally simple and the java code is as follows:

package sample.ws.metro;

import javax.jws.WebService;
@WebService public class HelloWorldService {
public String hello(String name) { return "Hello, " + name;
}

}

I followed the instructions on the web to create the applicationContext.xml and web.xml file and deployed it to my Tomcat server.  However, I had a hard time getting this simple example to work.  In the end, I downloaded Netbeans and created the web service solution in that IDE.  Netbeans generated the applicationContext.xml and web.xml file for me and it worked like a charm.  Once I have those configuration files, I could easily do subsequent changes in Eclipse without any problems.  This demonstrated how tool support can make life a lot easier for developers.  Once the initial setup is completed, it’s much simpler to build and modify from a working solution.  Here are the configuration files for those who might be interested:

applicationContext.xml:


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:ws="http://jax-ws.dev.java.net/spring/core"
xmlns:wss="http://jax-ws.dev.java.net/spring/servlet"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
http://jax-ws.dev.java.net/spring/core http://jax-ws.dev.java.net/spring/core.xsd
http://jax-ws.dev.java.net/spring/servlet http://jax-ws.dev.java.net/spring/servlet.xsd">

<wss:binding url="/helloworld">
<wss:service>
<ws:service bean="#HelloWorldService">
<ref bean="#HelloWorldService">
</ref>
</ws:service>
</wss:service>
</wss:binding>

<!-- this bean implements web service methods -->
<bean id="HelloWorldService" class="sample.ws.metro.HelloWorldService" />
</beans>

 web.xml:


<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
<display-name>metropilot</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<!-- this is for Spring -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>*.htm</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>
30
</session-timeout>
</session-config>
<!-- these are for JAX-WS -->
<servlet>
<servlet-name>jaxws-servlet</servlet-name>
<servlet-class>com.sun.xml.ws.transport.http.servlet.WSSpringServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>jaxws-servlet</servlet-name>
<url-pattern>/ws</url-pattern>
</servlet-mapping>
</web-app>

Similarly, on the Silverlight side…I needed the tools support in Silverlight on Visual Studio 2008 to help me generate the Service Reference library.  I opened up Visual Studio 2008, right-click my Silverlight library project and chose Add Service Reference… and then put in the url of my web service, which in my case was hosted at http://localhost:8080/metropilot/ws/hello?wsdl.  I would then build it as a Silverlight library that I can reference in the F# project.  I assume and hope that when Visual Studio 2010 arrives, I can do all of this without leaving the F# environment.  Now I can write my F# Silverlight client which asks the user to type in a name that will be sent to the web service and display the returned greeting.  The F# client code is as follows:


#light
namespace SilverLightFSharp

open System
open System.Windows
open System.Windows.Controls
open System.ServiceModel
open List

// Data Binding example
type MyPage = class
inherit UserControl

new () as this = {} then

let sp = new StackPanel()

let textblock = new TextBlock()
textblock.Text <- "Please enter your name:"
sp.Children.Add(textblock)

let textbox = new TextBox()
sp.Children.Add(textbox)


let button = new Button(Content="Get Greeting...")
sp.Children.Add(button)

button.Click.Add(fun e ->
let binding = new BasicHttpBinding();
let endpoint = new EndpointAddress("http://localhost:8080/metropilot/ws/hello?wsdl");
let service = new ServiceLibrary.MetroPilotService.HelloWorldServiceClient(binding,endpoint)
service.helloCompleted.Add(fun e ->
textblock.Text <- e.Result.ToString()
service.CloseAsync())
service.helloAsync(textbox.Text))

this.Width <- 400.0
this.Height <- 300.0
this.Content <- sp
end

type MyApp = class
inherit Application

new () as this = {} then
this.Startup.Add(fun _ -> this.RootVisual <- new MyPage())
//base.Exit.Add( fun _ -> ()) //this.Application_Exit)
//this.InitializeComponent()
end

My application.xap file has the following files:

  • AppManifest.xaml
  • FSharp.Core.dll
  • ServiceLibrary.dll – this was the library generated using Visual Studio 2008 via Add Reference…
  • SilverlightFSharp.dll – the compiled client code

Since I’m running this Silverlight client on an Apache web server that’s running on port 8000 and my web services was running on a Tomcat server on port 8080, I had to create the clientaccesspolicy.xml and crossdomain.xml file and put it in the root folder of the Tomcat server or the Silverlight client would refuse to access the web service on my Tomcat server. 

My clientaccesspolicy.xml is as follows:


<?xml version="1.0" encoding="utf-8"?>
<access-policy>
<cross-domain-access>
<policy>
<allow-from http-request-headers="*">
<domain uri="*"/>
</allow-from>
<grant-to>
<resource path="/" include-subpaths="true"/>
</grant-to>
</policy>
</cross-domain-access>
</access-policy>

My crossdomain.xml file is as follows:


<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<allow-http-request-headers-from domain="*" headers="*"/>
</cross-domain-policy>

Finally, after all these configurations, I got my F# Silverlight 2 client code running on Apache web server talking to my Java based web services running on Tomcat.