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)

Comments are closed.