/ F#

Quickstart WPF F#-only app in VSCode

Almost every aspect of the WPF application is customisable. It also saves hours by providing data-binding mechanism. And it is much more easy to understand code that was created following the MVVC pattern. Mainly because that this knowledge is shared and if you know this pattern then you know what to expect from the application that follows it.

F# adds value too. It is concise, functional and picky about nulls and mutability. The type providers is a great time-saver as well.

The editor is a forge. There are many, but three are significally better for Windows-specific programming. VS Studio, Rider and VS Code. The later has very small footprint and free. Although it is very powerful and provides Visual Studio level of tooling in some areas, it is still very close to the ground. It is more a text editor than IDE, which makes it a good choice for some projects.

So let's load the matherials (F#, WPF, Friday evening) and skils (me) to the forge (VS Code) and see what happens.

Note: Installed VS Code with lonide-fsharp is required to continue.

The project that I want to create is a useful application shows large files and folders that occupies a lot of space.

Perhaps the most easy way to start is to create the solution and project file from the Visual Studio and then switch to the VS Code.

Here is the initial file structure of the project:

LargeFilesWatch\
  App.config
  App.xaml
  AssemblyInfo.fs
  LargeFilesWatch.fsproj
  LargeFilesWatch.sln
  MainViewModel.fs
  MainWindow.xaml
  Program.fs

Unfortunately, the initial files should be created manually. As far as I know there is no tooling for that (except using Visual Studio, of course).

The initial content of the files is below.

LargeFilesWatch.sln

That's the typical Visual Studio Solution file. Although it is not realy necessary for this example as the fsproj file can be built directly, it is nice to have it for the future.

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.25420.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "LargeFilesWatch", "LargeFilesWatch.fsproj", "{12D6EA2C-85C8-4950-A3EC-4E01EEDC3CFC}"
EndProject
Global
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
		Debug|Any CPU = Debug|Any CPU
		Release|Any CPU = Release|Any CPU
	EndGlobalSection
	GlobalSection(ProjectConfigurationPlatforms) = postSolution
		{12D6EA2C-85C8-4950-A3EC-4E01EEDC3CFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{12D6EA2C-85C8-4950-A3EC-4E01EEDC3CFC}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{12D6EA2C-85C8-4950-A3EC-4E01EEDC3CFC}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{12D6EA2C-85C8-4950-A3EC-4E01EEDC3CFC}.Release|Any CPU.Build.0 = Release|Any CPU
	EndGlobalSection
	GlobalSection(SolutionProperties) = preSolution
		HideSolutionNode = FALSE
	EndGlobalSection
EndGlobal

LargeFilesWatch.fsproj

Contains MSBuild instructions, defines list and order of the compiled files, references libraries and other projects.

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <SchemaVersion>2.0</SchemaVersion>
    <ProjectGuid>{12D6EA2C-85C8-4950-A3EC-4E01EEDC3CFC}</ProjectGuid>
    <OutputType>WinExe</OutputType>
    <RootNamespace>LargeFilesWatch</RootNamespace>
    <AssemblyName>LargeFilesWatch</AssemblyName>
    <TargetFrameworkVersion>v4.7.1</TargetFrameworkVersion>
    <WarningLevel>3</WarningLevel>
    <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
    <TargetFSharpCoreVersion>4.4.0.0</TargetFSharpCoreVersion>
    <Name>LargeFilesWatch</Name>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <Tailcalls>false</Tailcalls>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <WarningLevel>3</WarningLevel>
    <DocumentationFile>bin\Debug\LargeFilesWatch.xml</DocumentationFile>
    <Prefer32Bit>true</Prefer32Bit>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <Tailcalls>true</Tailcalls>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <WarningLevel>3</WarningLevel>
    <DocumentationFile>bin\Release\LargeFilesWatch.xml</DocumentationFile>
    <Prefer32Bit>true</Prefer32Bit>
  </PropertyGroup>
  <PropertyGroup>
    <MinimumVisualStudioVersion Condition="'$(MinimumVisualStudioVersion)' == ''">11</MinimumVisualStudioVersion>
  </PropertyGroup>
  <Choose>
    <When Condition="'$(VisualStudioVersion)' == '11.0'">
      <PropertyGroup Condition="Exists('$(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets')">
        <FSharpTargetsPath>$(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets</FSharpTargetsPath>
      </PropertyGroup>
    </When>
    <Otherwise>
      <PropertyGroup Condition="Exists('$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets')">
        <FSharpTargetsPath>$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets</FSharpTargetsPath>
      </PropertyGroup>
    </Otherwise>
  </Choose>
  <Import Project="$(FSharpTargetsPath)" />
  <ItemGroup>
    <None Include="App.config" />
    <Resource Include="App.xaml" />
    <Compile Include="AssemblyInfo.fs" />
    <Resource Include="MainWindow.xaml" />
    <Compile Include="MainViewModel.fs" />
    <Compile Include="Program.fs" />
  </ItemGroup>
  <ItemGroup>
    <Reference Include="mscorlib" />
    <Reference Include="FSharp.Core, Version=$(TargetFSharpCoreVersion), Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
      <Private>True</Private>
    </Reference>
    <Reference Include="System" />
    <Reference Include="System.Data" />
    <Reference Include="System.Xml" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Core" />
    <Reference Include="System.Numerics" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="System.Net.Http" />
    <Reference Include="System.Xaml">
      <RequiredTargetFramework>4.0</RequiredTargetFramework>
    </Reference>
    <Reference Include="WindowsBase" />
    <Reference Include="PresentationCore" />
    <Reference Include="PresentationFramework" />
  </ItemGroup>
</Project>

app.config

Runtime configuration settings.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.1" />
  </startup>
</configuration>

AssemblyInfo.fs

Another file which content you can basically ignore. Just copy-paste its content to the local file.

namespace LargeFilesWatch.AssemblyInfo

open System.Reflection
open System.Runtime.InteropServices

[<assembly: AssemblyTitle("LargeFilesWatch")>]
[<assembly: AssemblyDescription("")>]
[<assembly: AssemblyConfiguration("")>]
[<assembly: AssemblyCompany("")>]
[<assembly: AssemblyProduct("LargeFilesWatch")>]
[<assembly: AssemblyCopyright("Copyright © Alex Netkachov 2017")>]
[<assembly: AssemblyTrademark("")>]
[<assembly: AssemblyCulture("")>]
[<assembly: ComVisible(false)>]
[<assembly: Guid("2ed00444-5876-4cf2-ad1a-ada160dc2cde")>]
[<assembly: AssemblyVersion("1.0.0.0")>]
[<assembly: AssemblyFileVersion("1.0.0.0")>]

do
    ()

Program.fs

It becomes more interesting. This is starting point of the application. Reads App.xaml and starts the app.

open System
open System.Windows
open System.Windows.Controls
open System.Windows.Markup

[<STAThread>]
[<EntryPoint>]
let main(_) =
  let application = Application.LoadComponent(new Uri("App.xaml", UriKind.Relative)) :?> Application
  application.Run()
  0

App.xaml

Minimalistic App.xaml.

<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:LargeFilesWatch"
             StartupUri="MainWindow.xaml" />

MainWindow.xaml

The main form of the application. Specifies which class is used as DataContext, displays welcome message.

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:LargeFilesWatch;assembly=LargeFilesWatch"
        mc:Ignorable="d"
        Title="Large File Watch"
        Height="350"
        Width="525">
    <Window.DataContext>
        <local:MainViewModel />
    </Window.DataContext>
    <Grid>
        <TextBlock Text="{Binding WelcomeText}"/>
    </Grid>
</Window>

MainViewModel.fs

View model of the MainWindow.xaml. Provides it with the welcome text.

namespace LargeFilesWatch

type MainViewModel () =
  member vm.WelcomeText
    with get() = "Hi there. I'm a new awesome app!"

The project can be build with the following MSBuild command:

PS c:\...> msbuild LargeFilesWatch.sln /p:VisualStudioVersion=14.0

If it fails with The term 'msbuild' is not recognized ... (in PowerShell), then the path c:\Windows\Microsoft.NET\Framework64\v4.0.30319 is not added to your environment variable PATH. Set it as follows and try to build the project again.

PS c:\...> $env:Path += ";c:\Windows\Microsoft.NET\Framework64\v4.0.30319"

If everything goes ok, the following should be printed at the end of the pretty large output:

Build succeeded.
    0 Warning(s)
    0 Error(s)

The command to run the app:

PS c:\...> .\bin\Debug\LargeFilesWatch.exe

All these commands can be executed from the integrated VS Code terminal.

Now let's add some logic to the app. The goal is to view the size of the files and let's do it for the files in the current folder (directory).

Add the items list with the scrollbars to the main window:

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        ...>
    <Grid>
        <ScrollViewer>
            <ItemsControl ItemsSource="{Binding LargeFiles}" />
        </ScrollViewer>
    </Grid>
</Window>

and modify the main view model as follows:

type MainWindowViewModel () =
  let mutable largeFiles : string array = [| |]

  do
    let files = DirectoryInfo(".").GetFiles()
                |> Array.sortByDescending (fun item -> item.Length)
                |> Array.map (fun item -> sprintf "%s %d" item.Name item.Length)
    largeFiles <- files

  member vm.LargeFiles
    with get() = largeFiles
    and set value = largeFiles <- value

Build and run. Everything works:

Capture

Remember that F# files in the fsproj should go in the order of compilation and that the last one should be Program.fs. Add new fs files to fsproj file as "Compile" nodes. When adding new XAML files, add them as Resources.

Alex Netkachov

Alex Netkachov

Alex Netkachov is a Senior Software Developer, currently working in Central London on new generation of energy trading solutions for brokers, traders and exchanges.

Read More

Why not to stay updated if the subject is interesting? Join Telegram channel Alex@Net or follow alex_at_net on Twitter. Or just, use the comments form below.