Last time, we provided the components to actually make the SEO helpers into a real application.
This time, we make the application more friendly. In particular : default files and directories are created if they don’t exist yet. Then, instead of being forced to use a command window, this time, double clicking will suffice.
This will also allow us to see how to use events and Winforms (yup, no WPF !), and how to use functions to make form building a tad quicker.
We still compile the application as a console application for two reasons : first, this mean we can still use the command-line calls (particularly useful through .bat files when the same arguments are used all the time) ; secondly, the info (fetching this and that) is displayed in the console when the application runs.
Application
namespace SeoAnalysis open System open System.Collections.Generic open System.IO open System.Text open System.Windows.Forms open Seo open SeoAnalysis module App = let maxKeywordResults = ref 100u let maxIndexedLinks = ref 100u let maxBackLinks = ref 100u let outputDir = ref <| Path.Combine(appDir, "output") let websites = ref <| set[Path.Combine(appDir, "websites.txt")] let keywords = ref <| set [Path.Combine(appDir, "keywords.txt")] let filesToSet files = [|for file in files do for line in File.ReadAllLines(file, Text.Encoding.UTF8) do yield line.ToLower() |] |> Set.ofArray let searchEngine = ref Google.SEARCH_ENGINE let _searchEngine (s:string) = match s.ToLower() with | "bing" -> Bing.SEARCH_ENGINE | "yahoo" -> Yahoo.SEARCH_ENGINE | _ -> Google.SEARCH_ENGINE let specs = [ "--output-directory", ArgType.String (fun s -> outputDir := s), "Path of the output directory" "--engine", ArgType.String (fun s -> searchEngine := _searchEngine s), "Search engine (google, yahoo or bing)" "--keywords", ArgType.Int (fun i -> maxKeywordResults := uint32 i), "Maximum number of results fetched when searching keyword-related info" "--indexed-links", ArgType.Int (fun i -> maxIndexedLinks := uint32 i), "Maximum number of results fetched when searching indexed links" "--back-links", ArgType.Int (fun i -> maxBackLinks := uint32 i), "Maximum number of results fetched when searching back links" ] |> List.map (fun (sh, ty, desc) -> ArgInfo(sh, ty, desc)) // // Gui helpers // let _getFiles title = use dlg = new OpenFileDialog( CheckPathExists = true, CheckFileExists = true, InitialDirectory = appDir, Multiselect = true, Title=title ) match dlg.ShowDialog() with | DialogResult.OK -> dlg.FileNames | _ -> [||] let updateLabel acc (label:Label) = let buf = StringBuilder() for file in acc do let dir = Path.GetDirectoryName file let fileName = Path.GetFileName file let txt = if dir.Length > 20 then sprintf "%s...%s%c%s" dir.[0..5] dir.[dir.Length-11..dir.Length-1] Path.DirectorySeparatorChar fileName else file Printf.bprintf buf "%s%s" txt Environment.NewLine label.Text <- buf.ToString() let getFiles acc title label = let newFiles = _getFiles (sprintf "Select the %s files..." title) for file in newFiles do if not <| Set.contains file !acc then acc := Set.add file !acc updateLabel !acc label let _getDir title = use dlg = new FolderBrowserDialog(SelectedPath = appDir) match dlg.ShowDialog() with | DialogResult.OK -> dlg.SelectedPath | _ -> String.Empty let getDir acc title (label:Label) = acc := _getDir (sprintf "Select the %s directory..." title) label.Text <- !acc let filesPanel acc title h = let panel = new FlowLayoutPanel(AutoSize=true, FlowDirection=FlowDirection.LeftToRight) let select = new Button(AutoSize = true, Text=sprintf "Select %s..." title) let files = new Label(AutoSize = true) updateLabel !acc files let clear = new Button(AutoSize = true, Text = "Clear", Enabled = (Seq.length !acc <> 0)) panel.Controls.AddRange([|select; files; clear|]) select.Click.Add(fun _ -> getFiles acc title files clear.Enabled <- true ) clear.Click.Add(fun _ -> clear.Enabled <- false acc := Set.empty files.Text <- "" ) Event.merge select.Click clear.Click |> Event.add h panel let dirPanel acc title h = let panel = new FlowLayoutPanel(AutoSize=true, FlowDirection=FlowDirection.LeftToRight) let select = new Button(AutoSize = true, Text=sprintf "Select %s..." title) let dir = new Label(AutoSize = true) dir.Text <- !acc let clear = new Button(AutoSize = true, Text = "Clear", Enabled = (String.length !acc <> 0)) panel.Controls.AddRange([|select; dir; clear|]) select.Click.Add(fun _ -> getDir acc title dir clear.Enabled <- true ) clear.Click.Add(fun _ -> clear.Enabled <- false acc := "" dir.Text <- "" ) Event.merge select.Click clear.Click |> Event.add h panel let updownPanel desc startValue h = let updown = new NumericUpDown( ThousandsSeparator = true, Minimum = 0M, Maximum = System.Decimal.MaxValue, Value = startValue, DecimalPlaces = 0, Increment = 50M, Width = 75 ) updown.Click.Add(fun _ -> h updown) let updownLabel = new Label(AutoSize = true, Text = desc) let updownPanel = new FlowLayoutPanel(AutoSize = true, FlowDirection=FlowDirection.LeftToRight) updownPanel.Controls.AddRange([|updownLabel; updown|]) updownPanel let performMain() = let websites = filesToSet !websites let keywords = filesToSet !keywords async { do! IndexedLinks.run !searchEngine websites !maxIndexedLinks !outputDir do! KeywordMatches.run !searchEngine keywords (Some websites) !maxKeywordResults !outputDir do! BackLinks.run !searchEngine websites !maxBackLinks !outputDir return () } [<STAThread>] do //make sure the default values exist... if not <| Directory.Exists(!outputDir) then Directory.CreateDirectory !outputDir |> ignore if not <| File.Exists(Seq.head !websites) then File.Create(Seq.head !websites) |> ignore if not <| File.Exists(Seq.head !keywords) then File.Create(Seq.head !keywords) |> ignore //see if we are using the command line arguments //first arg = app name --> ignore it //0 optional args ==> form shws up //some optional arg ==> run with default values & args let args = Environment.GetCommandLineArgs() if args.Length > 1 then ArgParser.Parse(specs) performMain() |> Async.RunSynchronously else let form = new Form( AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, Text = "SEO parameters" ) let panel = new FlowLayoutPanel( AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, Dock = DockStyle.Fill, FlowDirection = FlowDirection.TopDown ) let run = new Button(AutoSize = true, Text = "Run") form.Controls.Add panel let updateRun _ = run.Enabled <- Set.count !websites > 0 && Set.count !keywords > 0 && Directory.Exists(!outputDir) panel.Controls.AddRange( [|updownPanel "indexed links" 0M (fun updown -> maxIndexedLinks := uint32 updown.Value) updownPanel "keyword matches links" 500M (fun updown -> maxKeywordResults := uint32 updown.Value) updownPanel "back links" 100M (fun updown -> maxBackLinks := uint32 updown.Value) dirPanel outputDir "output" updateRun filesPanel websites "websites" updateRun filesPanel keywords "keywords" updateRun run |]) let rec task = async { let! _ = Async.AwaitEvent run.Click run.Enabled <- false MessageBox.Show("Beginning...") |> ignore do! performMain() MessageBox.Show("Done !") |> ignore run.Enabled <- true return! task } Async.StartImmediate task Application.EnableVisualStyles() Application.Run(form)