├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md └── Stamps ├── DataAccess ├── AssemblyInfo.fs ├── DataAccess.fsproj └── WebServices.fs ├── Model ├── AssemblyInfo.fs ├── Model.fsproj ├── Prelude.fs └── StampCollection.fs ├── Stamps.sln ├── View ├── App.config ├── App.xaml ├── App.xaml.cs ├── ApplicationModel.cs ├── Exceptions.cs ├── MainWindow.xaml ├── MainWindow.xaml.cs ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Settings.Designer.cs │ └── Settings.settings ├── StampsDemo.csproj ├── VisibilityConverter.cs └── packages.config └── ViewModel ├── AssemblyInfo.fs ├── Messaging.fs ├── StampCollectionViewModel.fs └── ViewModel.fsproj /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Roslyn cache directories 10 | *.ide 11 | 12 | # Build results 13 | 14 | [Dd]ebug/ 15 | [Rr]elease/ 16 | x64/ 17 | build/ 18 | [Bb]in/ 19 | [Oo]bj/ 20 | 21 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets 22 | !packages/*/build/ 23 | 24 | # MSTest test Results 25 | [Tt]est[Rr]esult*/ 26 | [Bb]uild[Ll]og.* 27 | 28 | *_i.c 29 | *_p.c 30 | *.ilk 31 | *.meta 32 | *.obj 33 | *.pch 34 | *.pdb 35 | *.pgc 36 | *.pgd 37 | *.rsp 38 | *.sbr 39 | *.tlb 40 | *.tli 41 | *.tlh 42 | *.tmp 43 | *.tmp_proj 44 | *.log 45 | *.vspscc 46 | *.vssscc 47 | .builds 48 | *.pidb 49 | *.svclog 50 | *.scc 51 | 52 | # Visual C++ cache files 53 | ipch/ 54 | *.aps 55 | *.ncb 56 | *.opensdf 57 | *.sdf 58 | *.cachefile 59 | 60 | # Visual Studio profiler 61 | *.psess 62 | *.vsp 63 | *.vspx 64 | 65 | # Guidance Automation Toolkit 66 | *.gpState 67 | 68 | # ReSharper is a .NET coding add-in 69 | _ReSharper*/ 70 | *.[Rr]e[Ss]harper 71 | 72 | # TeamCity is a build add-in 73 | _TeamCity* 74 | 75 | # DotCover is a Code Coverage Tool 76 | *.dotCover 77 | 78 | # NCrunch 79 | *.ncrunch* 80 | .*crunch*.local.xml 81 | 82 | # Installshield output folder 83 | [Ee]xpress/ 84 | 85 | # DocProject is a documentation generator add-in 86 | DocProject/buildhelp/ 87 | DocProject/Help/*.HxT 88 | DocProject/Help/*.HxC 89 | DocProject/Help/*.hhc 90 | DocProject/Help/*.hhk 91 | DocProject/Help/*.hhp 92 | DocProject/Help/Html2 93 | DocProject/Help/html 94 | 95 | # Click-Once directory 96 | publish/ 97 | 98 | # Publish Web Output 99 | *.Publish.xml 100 | 101 | # Enable nuget.exe in the .nuget folder (though normally executables are not tracked) 102 | !.nuget/NuGet.exe 103 | 104 | # Windows Azure Build Output 105 | csx 106 | *.build.csdef 107 | 108 | # Windows Store app package directory 109 | AppPackages/ 110 | 111 | # Others 112 | sql/ 113 | *.Cache 114 | ClientBin/ 115 | [Ss]tyle[Cc]op.* 116 | ~$* 117 | *~ 118 | *.dbmdl 119 | *.[Pp]ublish.xml 120 | *.pfx 121 | *.publishsettings 122 | 123 | # RIA/Silverlight projects 124 | Generated_Code/ 125 | 126 | # Backup & report files from converting an old project file to a newer 127 | # Visual Studio version. Backup files are not needed, because we have git ;-) 128 | _UpgradeReport_Files/ 129 | Backup*/ 130 | UpgradeLog*.XML 131 | UpgradeLog*.htm 132 | 133 | # SQL Server files 134 | App_Data/*.mdf 135 | App_Data/*.ldf 136 | 137 | 138 | #LightSwitch generated files 139 | GeneratedArtifacts/ 140 | _Pvt_Extensions/ 141 | ModelManifest.xml 142 | 143 | # ========================= 144 | # Windows detritus 145 | # ========================= 146 | 147 | # Windows image file caches 148 | Thumbs.db 149 | ehthumbs.db 150 | 151 | # Folder config file 152 | Desktop.ini 153 | 154 | # Recycle Bin used on file shares 155 | $RECYCLE.BIN/ 156 | 157 | # Mac desktop service store files 158 | .DS_Store 159 | 160 | # =================================================== 161 | # Exclude F# project specific directories and files 162 | # =================================================== 163 | 164 | # NuGet Packages Directory 165 | packages/ 166 | 167 | # Generated documentation folder 168 | docs/output/ 169 | 170 | # Temp folder used for publishing docs 171 | temp/ 172 | 173 | # Test results produced by build 174 | TestResults.xml 175 | 176 | # Nuget outputs 177 | nuget/*.nupkg 178 | src/FSharpVSPowerTools/Properties/AssemblyInfo.cs 179 | *.orig 180 | .paket/paket.exe 181 | .paket/*.exe.config 182 | paket-files 183 | *.xml 184 | 185 | release.cmd 186 | 187 | tests/data/*/*.dll 188 | tests/data/*/*.xml 189 | tests/data/*/*.pdb 190 | 191 | _NCrunch_* 192 | build_PRIVATE.cmd 193 | gallerycredentials.txt 194 | .fake 195 | 196 | .vs/config/applicationhost.config 197 | *.FSharpLint 198 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Savelenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # functional-mvvm 2 | 3 | This is a small demonstration of an MVVM architecture in FP style. Although the efforts of existing well-known F# projects dealing with MVVM and MVC are commendable, I personally wanted something more idiomatic. It is a shame _not_ to aim for an architecture, where state and mutability is confined as much as possible to the layer which is inherently stateful and mutable -- the view. 4 | 5 | A short search did not lead to any satisfying results, so I developed this architecture by combining the good properties of MVVM and the [Elm architecture](http://guide.elm-lang.org/architecture/index.html). One thing about the Elm architecture which I wanted to avoid is the same "problem" as with MVC: the model must contain state related to view-only considerations. A simple example of this is when data can be _displayed_ sorted in various ways, while the model does not care about sorting, according to its domain definition. Thus, MVVM is the way to go, where the view model is, by definition, the place for such "noise". 6 | 7 | The result is a small WPF application where both the view and view model (written in F#) are pure and only the view (written in C#) is unavoidably stateful. Comments, questions and suggestions are welcome (in the issue section). -------------------------------------------------------------------------------- /Stamps/DataAccess/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | namespace DataAccess.AssemblyInfo 2 | 3 | open System.Reflection 4 | open System.Runtime.CompilerServices 5 | open System.Runtime.InteropServices 6 | 7 | // General Information about an assembly is controlled through the following 8 | // set of attributes. Change these attribute values to modify the information 9 | // associated with an assembly. 10 | [] 11 | [] 12 | [] 13 | [] 14 | [] 15 | [] 16 | [] 17 | [] 18 | 19 | // Setting ComVisible to false makes the types in this assembly not visible 20 | // to COM components. If you need to access a type in this assembly from 21 | // COM, set the ComVisible attribute to true on that type. 22 | [] 23 | 24 | // The following GUID is for the ID of the typelib if this project is exposed to COM 25 | [] 26 | 27 | // Version information for an assembly consists of the following four values: 28 | // 29 | // Major Version 30 | // Minor Version 31 | // Build Number 32 | // Revision 33 | // 34 | // You can specify all the values or you can default the Build and Revision Numbers 35 | // by using the '*' as shown below: 36 | // [] 37 | [] 38 | [] 39 | 40 | do 41 | () -------------------------------------------------------------------------------- /Stamps/DataAccess/DataAccess.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | 2.0 8 | 420164b6-3844-45d4-8042-36effb3c7bd7 9 | Library 10 | DataAccess 11 | DataAccess 12 | v4.6.1 13 | 4.4.0.0 14 | true 15 | DataAccess 16 | 17 | 18 | true 19 | full 20 | false 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | 3 25 | bin\Debug\DataAccess.XML 26 | 27 | 28 | pdbonly 29 | true 30 | true 31 | bin\Release\ 32 | TRACE 33 | 3 34 | bin\Release\DataAccess.XML 35 | 36 | 37 | 38 | 39 | True 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | Model 52 | {148aee05-bb52-4a8b-a1cf-fbf707be52ec} 53 | True 54 | 55 | 56 | 57 | 11 58 | 59 | 60 | 61 | 62 | $(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets 63 | 64 | 65 | 66 | 67 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets 68 | 69 | 70 | 71 | 72 | 79 | -------------------------------------------------------------------------------- /Stamps/DataAccess/WebServices.fs: -------------------------------------------------------------------------------- 1 | namespace Stamps.DataAccess 2 | 3 | open System 4 | open Stamps.Model 5 | 6 | type WorldStampKnowledgeBaseService() = 7 | 8 | interface IStampRarenessService with 9 | 10 | member svc.VerifyRareness(description, value) = 11 | 12 | (* A stamp is considered rare, if its value is not divisible by 5. However, it takes the service quite 13 | some time to calculate the answer. Not enough cores or something... *) 14 | async { 15 | // simulate not enough cores 16 | Async.Sleep 10000 |> Async.RunSynchronously |> ignore 17 | 18 | return (description, if int value % 5 = 0 then Rareness.VerifiedNotRare else VerifiedRare) 19 | } -------------------------------------------------------------------------------- /Stamps/Model/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | namespace Model.AssemblyInfo 2 | 3 | open System.Reflection 4 | open System.Runtime.CompilerServices 5 | open System.Runtime.InteropServices 6 | 7 | // General Information about an assembly is controlled through the following 8 | // set of attributes. Change these attribute values to modify the information 9 | // associated with an assembly. 10 | [] 11 | [] 12 | [] 13 | [] 14 | [] 15 | [] 16 | [] 17 | [] 18 | 19 | // Setting ComVisible to false makes the types in this assembly not visible 20 | // to COM components. If you need to access a type in this assembly from 21 | // COM, set the ComVisible attribute to true on that type. 22 | [] 23 | 24 | // The following GUID is for the ID of the typelib if this project is exposed to COM 25 | [] 26 | 27 | // Version information for an assembly consists of the following four values: 28 | // 29 | // Major Version 30 | // Minor Version 31 | // Build Number 32 | // Revision 33 | // 34 | // You can specify all the values or you can default the Build and Revision Numbers 35 | // by using the '*' as shown below: 36 | // [] 37 | [] 38 | [] 39 | 40 | do 41 | () -------------------------------------------------------------------------------- /Stamps/Model/Model.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | 2.0 8 | 148aee05-bb52-4a8b-a1cf-fbf707be52ec 9 | Library 10 | Model 11 | Model 12 | v4.6.1 13 | 4.4.0.0 14 | true 15 | Model 16 | 17 | 18 | true 19 | full 20 | false 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | 3 25 | bin\Debug\Model.XML 26 | 27 | 28 | pdbonly 29 | true 30 | true 31 | bin\Release\ 32 | TRACE 33 | 3 34 | bin\Release\Model.XML 35 | 36 | 37 | 38 | 39 | True 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 11 52 | 53 | 54 | 55 | 56 | $(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets 57 | 58 | 59 | 60 | 61 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets 62 | 63 | 64 | 65 | 66 | 73 | -------------------------------------------------------------------------------- /Stamps/Model/Prelude.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Prelude 3 | 4 | /// Exchanges parameters of a binary function. 5 | let flip f b a = f a b 6 | 7 | /// Injects the second argument into a tuple in left position. 8 | let injectL r l = (l,r) 9 | 10 | /// Injects the second argument into a tuple in right position. 11 | let injectR l r = (l,r) -------------------------------------------------------------------------------- /Stamps/Model/StampCollection.fs: -------------------------------------------------------------------------------- 1 | namespace Stamps.Model 2 | 3 | open System 4 | 5 | (* The model. Its (simplified) domain is stamp collecting. *) 6 | 7 | (* A stamp may be rare. This must be verified. *) 8 | 9 | type Rareness = 10 | | Unknown 11 | | VerifiedRare 12 | | VerifiedNotRare 13 | 14 | (* In this model, a stamp has a description, an integer value and rareness. Description uniquely identifies a stamp. *) 15 | 16 | type Description = string 17 | 18 | type Stamp = 19 | { 20 | Description : Description 21 | Value : uint32 22 | Rareness : Rareness 23 | } 24 | 25 | (* The following types represents the fact that a stamp is owned. The benefit of this, among others, is that we can 26 | "share" rareness value among all exemplars of one stamp which is in the collection. *) 27 | 28 | type OwnedStamp = OwnedStamp of stamp : Stamp * numberOwned : uint32 29 | 30 | (* Verification of stamp rareness can be performed by the imaginary "world stamp knowledgebase service". By applying 31 | the dependency inversion principle, the model defines how it wishes to consume the service. *) 32 | 33 | type IStampRarenessService = 34 | 35 | (* In order to be pure, the model is not allowed to verify stamp rareness directly, using this service. 36 | Instead, we create a definition of this action, which requires an effectful computation and will be performed 37 | elsewhere, outside the model. The result of this computation must then somehow find its way to the model, which 38 | will be shown in another component of the system. We use Async to define such computations. The nice thing 39 | (probably by design, exactly to support such a scenario as this) about F# Async is that definition of an Async 40 | value is strictly separated from its execution; due to this, it is easy to write pure functions involving Async. 41 | In contrast, .NET Task is supposed to be already running when returned by a function. *) 42 | 43 | abstract VerifyRareness : Description * uint32 -> Async 44 | 45 | (* "The" main model is the collection of owned stamps together with the rareness service. *) 46 | 47 | type StampCollection = 48 | { 49 | Stamps : Set 50 | RarenessService : IStampRarenessService 51 | } 52 | 53 | module Model = 54 | 55 | /// The initial model which the application may start with. 56 | let emptyCollection rarenessService = 57 | { 58 | Stamps = Set.empty 59 | RarenessService = rarenessService 60 | } 61 | 62 | /// A small pre-filled model which the application starts with. 63 | let smallCollection rarenessService = 64 | 65 | let stamp1 = { Description = "Aereo Nicaragua 1987"; Value = 30u; Rareness = Unknown } 66 | let stamp2 = { Description = "Comunicaciones cosmicas"; Value = 6u; Rareness = VerifiedRare } 67 | let stamp3 = { Description = "Shinkansen N700"; Value = 100u; Rareness = Unknown } 68 | 69 | let stamps = 70 | Set.empty 71 | |> Set.add (OwnedStamp (stamp1, 1u)) 72 | |> Set.add (OwnedStamp (stamp2, 1u)) 73 | |> Set.add (OwnedStamp (stamp3, 2u)) 74 | 75 | { 76 | Stamps = stamps 77 | RarenessService = rarenessService 78 | } 79 | 80 | 81 | (* In the next function we don't use type OwnedStamp for the convenience of the model consumer, i.e. the view 82 | model. We kind of consider type OwnedStamp to be an implementation detail of the model. *) 83 | 84 | /// Returns all stamp exemplars in the stamp collection. 85 | let allStamps collection = 86 | 87 | let allExemplars (OwnedStamp (stamp, number)) = List.replicate (int number) stamp 88 | 89 | collection.Stamps 90 | |> Seq.collect allExemplars 91 | |> List.ofSeq 92 | 93 | /// Adds a stamp to the stamp collection. 94 | let addStampToCollection description value collection = 95 | 96 | (* Given the definition that descriptions are unique, this function, together with function emptyCollection, 97 | maintains the invariant that there is at most one OwnedStamp in the collection for a given description. *) 98 | 99 | let desc (OwnedStamp ({ Description = d }, _)) = d 100 | 101 | let increaseOwnedQuantity = function 102 | | OwnedStamp (s, owned) as os when desc os = description -> OwnedStamp (s, owned + 1u) 103 | | otherStamp -> otherStamp 104 | 105 | let stamps = collection.Stamps 106 | if Set.exists (desc >> (=) description) stamps then 107 | { collection with Stamps = Set.map increaseOwnedQuantity stamps } 108 | else 109 | let withNewStamp = 110 | { Stamp.Description = description; Value = value; Rareness = Unknown } 111 | |> injectL 1u // one exemplar of the stamp in the collection 112 | |> OwnedStamp 113 | |> flip Set.add stamps 114 | { collection with Stamps = withNewStamp } 115 | 116 | (* The following two functions implement stamp rareness verification. Note how the result type of the effectful 117 | computation returned by the first function occurs in the type of the second function. *) 118 | 119 | /// Computes rareness of the stamp with the given description. 120 | let verifyStampRareness description value collection = 121 | collection.RarenessService.VerifyRareness (description,value) 122 | 123 | /// Registers rareness of the stamp with the given description. The collection does not change if it contains no 124 | /// stamp with this description. 125 | let setStampRareness description rareness collection = 126 | 127 | (* Use the invariant that there is at most one OwnedStamp in the collection for a given description. *) 128 | 129 | let applyRareness' s = { s with Rareness = rareness } 130 | 131 | let desc (OwnedStamp ({ Description = d }, _)) = d 132 | 133 | let applyRareness = function 134 | | OwnedStamp (stamp, n) as os when desc os = description -> OwnedStamp (applyRareness' stamp, n) 135 | | otherStamp -> otherStamp 136 | 137 | { collection with Stamps = Set.map applyRareness collection.Stamps } -------------------------------------------------------------------------------- /Stamps/Stamps.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Model", "Model\Model.fsproj", "{148AEE05-BB52-4A8B-A1CF-FBF707BE52EC}" 7 | EndProject 8 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "ViewModel", "ViewModel\ViewModel.fsproj", "{7ABE30DE-3C24-48CC-B1BC-842CC83971F9}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StampsDemo", "View\StampsDemo.csproj", "{1BED92F8-A648-418D-940C-9CD74AFB7F65}" 11 | EndProject 12 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "DataAccess", "DataAccess\DataAccess.fsproj", "{420164B6-3844-45D4-8042-36EFFB3C7BD7}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {148AEE05-BB52-4A8B-A1CF-FBF707BE52EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {148AEE05-BB52-4A8B-A1CF-FBF707BE52EC}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {148AEE05-BB52-4A8B-A1CF-FBF707BE52EC}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {148AEE05-BB52-4A8B-A1CF-FBF707BE52EC}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {7ABE30DE-3C24-48CC-B1BC-842CC83971F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {7ABE30DE-3C24-48CC-B1BC-842CC83971F9}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {7ABE30DE-3C24-48CC-B1BC-842CC83971F9}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {7ABE30DE-3C24-48CC-B1BC-842CC83971F9}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {1BED92F8-A648-418D-940C-9CD74AFB7F65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {1BED92F8-A648-418D-940C-9CD74AFB7F65}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {1BED92F8-A648-418D-940C-9CD74AFB7F65}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {1BED92F8-A648-418D-940C-9CD74AFB7F65}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {420164B6-3844-45D4-8042-36EFFB3C7BD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {420164B6-3844-45D4-8042-36EFFB3C7BD7}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {420164B6-3844-45D4-8042-36EFFB3C7BD7}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {420164B6-3844-45D4-8042-36EFFB3C7BD7}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | EndGlobal 41 | -------------------------------------------------------------------------------- /Stamps/View/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Stamps/View/App.xaml: -------------------------------------------------------------------------------- 1 |  6 | 7 | -------------------------------------------------------------------------------- /Stamps/View/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using Stamps.DataAccess; 2 | using System.Windows; 3 | 4 | namespace Stamps.View 5 | { 6 | /// 7 | /// Interaction logic for App.xaml 8 | /// 9 | public partial class App : Application 10 | { 11 | /// 12 | /// This serves as the main function of the complete application. It creates initial state, consisting of the 13 | /// initial model and view model, and ties together the view and the dispatching function through an 14 | /// object. All dependencies of the model, like our small data access layer, 15 | /// are also initialized here and passed to the model. I like the term "composition root" for describing this 16 | /// place in the system. 17 | /// 18 | /// 19 | /// 20 | private void Application_Startup(object sender, StartupEventArgs e) 21 | { 22 | // Create the initial model and view model. 23 | var stampService = new WorldStampKnowledgeBaseService(); 24 | var initialModel = Stamps.Model.Model.smallCollection(stampService); 25 | var initialViewModel = Stamps.ViewModel.ViewModel.initialViewModel(initialModel); 26 | 27 | var application = new ApplicationModel(initialModel, initialViewModel); 28 | var mainWindow = new MainWindow(initialViewModel, application); 29 | mainWindow.Show(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Stamps/View/ApplicationModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Linq; 3 | using System.Reactive.Subjects; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.FSharp.Control; 7 | using Microsoft.FSharp.Core; 8 | using Stamps.Messaging; 9 | using Stamps.Model; 10 | using Stamps.ViewModel; 11 | 12 | namespace Stamps.View 13 | { 14 | /// 15 | /// 16 | /// This class is the main "effectful" component of the application, besides the view itself, which is inherently 17 | /// mutable and stateful. It contains current model and view model, passing them to the dispatcher function and 18 | /// replacing them with new model and view model when the dispatching function returns. It is also responsible for 19 | /// performing effectful computations requested by the model and fitting this into the execution model of the 20 | /// view (things like GUI threads and so on). Lastly, this class provides to the view the possibility of sending 21 | /// messages. Note, that the view is not aware of the final destination of the messages. 22 | /// 23 | /// 24 | /// Intuitively, this component has nothing to do with the view; the using directives contain no dependency 25 | /// whatsoever on WPF. We can distinguish the layers of the system even more, by putting this class together with 26 | /// the "main" function of the system, separately from the view. However, in WPF it is idiomatic that the main 27 | /// function is in the view itself, so I kept this class in the view. 28 | /// 29 | /// 30 | public class ApplicationModel 31 | { 32 | /// 33 | /// Current model. 34 | /// 35 | private StampCollection model; 36 | 37 | /// 38 | /// Current view model. 39 | /// 40 | private StampCollectionViewModel viewModel; 41 | 42 | /// 43 | /// When the view sends a message, this may result in changes to the view. As we aim for the view to be 44 | /// completely derivable from the view model and the view model is immutable, we need a way for the view to 45 | /// "receive" the new view model. This Rx subject is used by this class to send new view models to the view. 46 | /// 47 | private readonly Subject viewModelSubject; 48 | 49 | /// 50 | /// Initializes the initial state of the application, consisting of the initial model and initial view model. 51 | /// 52 | /// 53 | /// Note, that as this class is merely a "state holder", it does not create the initial model and view model 54 | /// itself. This happens in the "main" function of the application. 55 | /// 56 | /// 57 | /// The initial model. 58 | /// 59 | /// 60 | /// The initial view model. 61 | /// 62 | internal ApplicationModel(StampCollection initialModel, StampCollectionViewModel initialViewModel) 63 | { 64 | this.model = initialModel; 65 | this.viewModel = initialViewModel; 66 | 67 | this.viewModelSubject = new Subject(); 68 | } 69 | 70 | /// 71 | /// This is the stream of view models, representing changes of the view model, accessible to the view. 72 | /// 73 | internal IObservable ViewModel => this.viewModelSubject.AsObservable(); 74 | 75 | /// 76 | /// The view sends messages using this method. This method is also used for messages originating from the world 77 | /// of effectful computations. In a way, this class also belongs to that world... 78 | /// 79 | /// 80 | internal void SendMessage(Message message) 81 | { 82 | // Invoke the dispatching function with current model, current view model and the message. 83 | var newMandVm = Dispatching.dispatch(this.model,this.viewModel, message); 84 | 85 | // Remember new model and view model as current model and view model, respectively. Observe, that this 86 | // class doesn't care whether the model or view model actually changed. 87 | this.model = newMandVm.Item1; 88 | this.viewModel = newMandVm.Item2; 89 | 90 | // If the model requested an effectful computation, perform it and route the message back to this function, 91 | // such that it will be passed to the dispatcher function, exactly the same way as for messages coming from 92 | // the view. 93 | var effect = newMandVm.Item3; 94 | if (effect.IsEffect) 95 | { 96 | var e = effect as Effect.Effect; 97 | ExecuteEffect(e.Item, this.SendMessage); // executes asynchronously (AE) 98 | } 99 | 100 | // Send the updated view model to the view. Property (AE) is crucial, because the GUI will not be blocked 101 | // and the view can assume any intermediate state defined by the view model, until the computation (if any) 102 | // is done. The example in this demo is simulated verification of stamp rareness, where the view displays a 103 | // "working on it" feed-back. 104 | this.viewModelSubject.OnNext(this.viewModel); 105 | } 106 | 107 | /// 108 | /// Implementation of the world of effectful computations. It respects the threading model of WPF. 109 | /// 110 | /// 111 | /// The effectful computation to be performed. 112 | /// 113 | /// 114 | /// A function which accepts the message produced by , when it completes. 115 | /// 116 | private static async void ExecuteEffect(FSharpAsync effect, Action messageDestination) 117 | { 118 | var m = await FSharpAsync.StartAsTask( 119 | effect, 120 | FSharpOption.None, 121 | FSharpOption.None); 122 | 123 | messageDestination(m); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Stamps/View/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Stamps.View 4 | { 5 | /// 6 | /// Some handy extension methods for less pain with exceptions in C#. 7 | /// 8 | public static class Exceptions 9 | { 10 | /// 11 | /// Allows to throw an exception inside of an expression, instead of inside of a statement only. 12 | /// 13 | /// 14 | /// The type of the expression. 15 | /// 16 | /// 17 | /// The exception to throw. 18 | /// 19 | /// 20 | /// Never returns, throws . 21 | /// 22 | public static T Throw(Exception e) 23 | { 24 | throw e; 25 | } 26 | 27 | /// 28 | /// A variant of as an extension method. 29 | /// 30 | public static T Throw(this Object obj, Exception e) 31 | { 32 | throw e; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Stamps/View/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 49 | Value low to high 50 | 51 | 53 | Value high to low 54 | 55 | 57 | None 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 70 | Working... 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /Stamps/View/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | using System.Windows.Controls; 4 | using Stamps.Messaging; 5 | using Stamps.ViewModel; 6 | 7 | namespace Stamps.View 8 | { 9 | /// 10 | /// The main type of the view layer. This WPF code-behind component translates GUI events to messages and renders 11 | /// the view model when it "changes". 12 | /// 13 | public partial class MainWindow : Window 14 | { 15 | /// 16 | /// We need a reference to for accessing 17 | /// . 18 | /// 19 | private readonly ApplicationModel applicationModel; 20 | 21 | /* The following three helper values are used for rendering the state of stamp display order. */ 22 | 23 | private static readonly StampDisplayOrder SortedValueHighToLow = 24 | StampDisplayOrder.NewSorted(StampSortedDisplayOrder.ByValueDescending); 25 | 26 | private static readonly StampDisplayOrder SortedValueLowToHigh = 27 | StampDisplayOrder.NewSorted(StampSortedDisplayOrder.ByValueAscending); 28 | 29 | private static readonly StampDisplayOrder NotSorted = StampDisplayOrder.Unsorted; 30 | 31 | /// 32 | /// Initializes the main view (window) and renders the initial view based on the initial view model. 33 | /// 34 | /// 35 | /// The initial view model. 36 | /// 37 | /// 38 | /// The object which provides updates of the view model and a way of sending messages. 39 | /// 40 | public MainWindow(StampCollectionViewModel initialViewModel, ApplicationModel applicationModel) 41 | { 42 | InitializeComponent(); 43 | 44 | this.applicationModel = applicationModel; 45 | 46 | // Render the initial view. 47 | this.Render(initialViewModel); // (IR) 48 | 49 | // Subscribe to changes in the view model. Each time a new view model arrives, render it. 50 | this.applicationModel.ViewModel.Subscribe(this.Render); 51 | 52 | /* We could avoid initial explicit call for initial rendering (IR) by using some fancy Rx subject which 53 | * sends the initial model already upon subscription. I kept things simple here, because this is an 54 | * irrelevant implementation detail. */ 55 | } 56 | 57 | /// 58 | /// Renders the view based on the provided view model. 59 | /// 60 | /// 61 | /// The view model which must be reflected by the view. 62 | /// 63 | private void Render(StampCollectionViewModel viewModel) 64 | { 65 | /* In this case, I use a mixture of WPF data binding paradigm and manual WPF element manipulation. The key 66 | * point is that this is merely an implementation detail of the view and uses view-specific idioms and 67 | * "technology". The view model is completely unaware of this, which is good, because usually GUI 68 | * frameworks are stateful and mutable. */ 69 | 70 | this.DataContext = viewModel; 71 | 72 | /* Some things are easier to make not using WPF data binding. The following fragment displays current 73 | * stamp display order using the corresponding radio buttons. */ 74 | // { none of the radio buttons is checked }, by intention 75 | if (viewModel.DisplayOrder.Equals(SortedValueHighToLow)) 76 | { 77 | this.rbSortHighToLow.IsChecked = true; 78 | } 79 | else if (viewModel.DisplayOrder.Equals(SortedValueLowToHigh)) 80 | { 81 | this.rbSortLowToHigh.IsChecked = true; 82 | } 83 | else if (viewModel.DisplayOrder.Equals(NotSorted)) 84 | { 85 | this.rbSortNone.IsChecked = true; 86 | } 87 | } 88 | 89 | private void AddNewStampButtonClick(object sender, RoutedEventArgs e) 90 | { 91 | var description = this.newStampDescription.Text; 92 | 93 | // Admittedly, this validation should be performed in the view model or even model, but I kept this for 94 | // simplicity; it does not distract from the main idea of the architecture. 95 | uint value; 96 | if (UInt32.TryParse(this.newStampValue.Text, out value)) 97 | { 98 | // Translate this GUI event to the corresponding message and send it. 99 | this.applicationModel.SendMessage(Message.NewAddStamp(description, value)); 100 | } 101 | else 102 | { 103 | this.newStampValue.Text = "!!"; 104 | } 105 | 106 | } 107 | 108 | private void StampSortingRadioButtonClick(object sender, RoutedEventArgs e) 109 | { 110 | /* Something interesting happens here. WPF jumps into conclusion by deriving the new state of radio 111 | * buttons when we merely click on them. We don't want this side effect, because the state of the view must 112 | * be derived from the view model. So we discard the state of radio buttons induced by WPF in the unwelcome 113 | * manner, until the view model changes. Only then render the new state of radio buttons, see method 114 | * Render. This idea is much deeper than it seems and leads to some very interesting questions, but this is 115 | * just a simple demo, so let's not go there... */ 116 | var rb = (RadioButton)sender; 117 | rb.IsChecked = false; 118 | 119 | // Prepare a message based on which of the radio buttons exactly was clicked. 120 | var message = 121 | rb == this.rbSortHighToLow ? Message.NewSortStamps(StampSortedDisplayOrder.ByValueDescending) : 122 | rb == this.rbSortLowToHigh ? Message.NewSortStamps(StampSortedDisplayOrder.ByValueAscending) : 123 | rb == this.rbSortNone ? Message.RemoveStampSorting : 124 | this.Throw(new ArgumentException("Event handler attached to wrong element.")); 125 | 126 | // Send the message. 127 | this.applicationModel.SendMessage(message); 128 | } 129 | 130 | private void ButtonCheckRarenessClick(object sender, RoutedEventArgs e) 131 | { 132 | /* Similar to other event handlers, nothing special here. */ 133 | var stamp = (sender as Button).DataContext as StampViewModel; 134 | 135 | var message = Message.NewValidateRareness(stamp.Description, stamp.Value); 136 | 137 | this.applicationModel.SendMessage(message); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Stamps/View/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Resources; 3 | using System.Runtime.CompilerServices; 4 | using System.Runtime.InteropServices; 5 | using System.Windows; 6 | 7 | // General Information about an assembly is controlled through the following 8 | // set of attributes. Change these attribute values to modify the information 9 | // associated with an assembly. 10 | [assembly: AssemblyTitle("View")] 11 | [assembly: AssemblyDescription("")] 12 | [assembly: AssemblyConfiguration("")] 13 | [assembly: AssemblyCompany("")] 14 | [assembly: AssemblyProduct("View")] 15 | [assembly: AssemblyCopyright("Copyright © 2016")] 16 | [assembly: AssemblyTrademark("")] 17 | [assembly: AssemblyCulture("")] 18 | 19 | // Setting ComVisible to false makes the types in this assembly not visible 20 | // to COM components. If you need to access a type in this assembly from 21 | // COM, set the ComVisible attribute to true on that type. 22 | [assembly: ComVisible(false)] 23 | 24 | //In order to begin building localizable applications, set 25 | //CultureYouAreCodingWith in your .csproj file 26 | //inside a . For example, if you are using US english 27 | //in your source files, set the to en-US. Then uncomment 28 | //the NeutralResourceLanguage attribute below. Update the "en-US" in 29 | //the line below to match the UICulture setting in the project file. 30 | 31 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] 32 | 33 | 34 | [assembly: ThemeInfo( 35 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 36 | //(used if a resource is not found in the page, 37 | // or application resource dictionaries) 38 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 39 | //(used if a resource is not found in the page, 40 | // app, or any theme specific resource dictionaries) 41 | )] 42 | 43 | 44 | // Version information for an assembly consists of the following four values: 45 | // 46 | // Major Version 47 | // Minor Version 48 | // Build Number 49 | // Revision 50 | // 51 | // You can specify all the values or you can default the Build and Revision Numbers 52 | // by using the '*' as shown below: 53 | // [assembly: AssemblyVersion("1.0.*")] 54 | [assembly: AssemblyVersion("1.0.0.0")] 55 | [assembly: AssemblyFileVersion("1.0.0.0")] 56 | -------------------------------------------------------------------------------- /Stamps/View/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace Stamps.View.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Stamps.View.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Stamps/View/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | text/microsoft-resx 107 | 108 | 109 | 2.0 110 | 111 | 112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 113 | 114 | 115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | -------------------------------------------------------------------------------- /Stamps/View/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace Stamps.View.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Stamps/View/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Stamps/View/StampsDemo.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {1BED92F8-A648-418D-940C-9CD74AFB7F65} 8 | WinExe 9 | Properties 10 | Stamps.View 11 | StampsDemo 12 | v4.6.1 13 | 512 14 | {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 15 | 4 16 | true 17 | 18 | 19 | AnyCPU 20 | true 21 | full 22 | false 23 | bin\Debug\ 24 | DEBUG;TRACE 25 | prompt 26 | 4 27 | 28 | 29 | AnyCPU 30 | pdbonly 31 | true 32 | bin\Release\ 33 | TRACE 34 | prompt 35 | 4 36 | 37 | 38 | Stamps.View.App 39 | 40 | 41 | 42 | 43 | 44 | 45 | ..\packages\System.Reactive.Core.3.0.0\lib\net46\System.Reactive.Core.dll 46 | True 47 | 48 | 49 | ..\packages\System.Reactive.Interfaces.3.0.0\lib\net45\System.Reactive.Interfaces.dll 50 | True 51 | 52 | 53 | ..\packages\System.Reactive.Linq.3.0.0\lib\net46\System.Reactive.Linq.dll 54 | True 55 | 56 | 57 | ..\packages\System.Reactive.PlatformServices.3.0.0\lib\net46\System.Reactive.PlatformServices.dll 58 | True 59 | 60 | 61 | ..\packages\System.Reactive.Windows.Threading.3.0.0\lib\net45\System.Reactive.Windows.Threading.dll 62 | True 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 4.0 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | MSBuild:Compile 80 | Designer 81 | 82 | 83 | 84 | 85 | MSBuild:Compile 86 | Designer 87 | 88 | 89 | App.xaml 90 | Code 91 | 92 | 93 | 94 | MainWindow.xaml 95 | Code 96 | 97 | 98 | 99 | 100 | Code 101 | 102 | 103 | True 104 | True 105 | Resources.resx 106 | 107 | 108 | True 109 | Settings.settings 110 | True 111 | 112 | 113 | ResXFileCodeGenerator 114 | Resources.Designer.cs 115 | 116 | 117 | 118 | SettingsSingleFileGenerator 119 | Settings.Designer.cs 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | {420164b6-3844-45d4-8042-36effb3c7bd7} 129 | DataAccess 130 | 131 | 132 | {148aee05-bb52-4a8b-a1cf-fbf707be52ec} 133 | Model 134 | 135 | 136 | {7abe30de-3c24-48cc-b1bc-842cc83971f9} 137 | ViewModel 138 | 139 | 140 | 141 | 148 | -------------------------------------------------------------------------------- /Stamps/View/VisibilityConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Windows; 4 | using System.Windows.Data; 5 | 6 | namespace Stamps.View 7 | { 8 | [ValueConversion(typeof(bool), typeof(Visibility))] 9 | internal class VisibilityConverter : IValueConverter 10 | { 11 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 12 | { 13 | var visible = (bool)value; 14 | 15 | return visible ? Visibility.Visible : Visibility.Hidden; 16 | } 17 | 18 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => 19 | Binding.DoNothing; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Stamps/View/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Stamps/ViewModel/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | namespace ViewModel.AssemblyInfo 2 | 3 | open System.Reflection 4 | open System.Runtime.CompilerServices 5 | open System.Runtime.InteropServices 6 | 7 | // General Information about an assembly is controlled through the following 8 | // set of attributes. Change these attribute values to modify the information 9 | // associated with an assembly. 10 | [] 11 | [] 12 | [] 13 | [] 14 | [] 15 | [] 16 | [] 17 | [] 18 | 19 | // Setting ComVisible to false makes the types in this assembly not visible 20 | // to COM components. If you need to access a type in this assembly from 21 | // COM, set the ComVisible attribute to true on that type. 22 | [] 23 | 24 | // The following GUID is for the ID of the typelib if this project is exposed to COM 25 | [] 26 | 27 | // Version information for an assembly consists of the following four values: 28 | // 29 | // Major Version 30 | // Minor Version 31 | // Build Number 32 | // Revision 33 | // 34 | // You can specify all the values or you can default the Build and Revision Numbers 35 | // by using the '*' as shown below: 36 | // [] 37 | [] 38 | [] 39 | 40 | do 41 | () -------------------------------------------------------------------------------- /Stamps/ViewModel/Messaging.fs: -------------------------------------------------------------------------------- 1 | namespace Stamps.Messaging 2 | 3 | open Stamps.Model 4 | open Stamps.ViewModel 5 | 6 | (* The view model and view are triggered using messages. A message originates from the view based on a GUI event. 7 | A message can also originate from the "effectful world" and carry a result of an effectful computation performed in 8 | that world, for example (EM1) below. The following type defines all possible messages. *) 9 | 10 | type Message = 11 | | AddStamp of description : string * value : uint32 12 | | ValidateRareness of description : string * value : uint32 13 | | RarenessValidated of description : string * rareness : Stamps.Model.Rareness // (EM1) 14 | | SortStamps of StampSortedDisplayOrder 15 | | RemoveStampSorting 16 | 17 | (* The model returns effectful computations as F# Async values. A bit further, we will need to distinguish situations 18 | when there is a computation to perform and when no action should be taken, beside application of model and view model 19 | functions. The following type contains corresponding values, where None designates no need to perform any effect. *) 20 | 21 | type Effect<'T> = 22 | | None 23 | | Effect of Async<'T> 24 | 25 | [] 26 | module Effect = 27 | 28 | /// Applies the given function to the result of the effectful computation. 29 | let map f = function 30 | | None -> None 31 | | Effect comp -> 32 | async { 33 | let! x = comp 34 | return f x 35 | } 36 | |> Effect 37 | 38 | module Dispatching = 39 | 40 | (* The dispatch function translates messages to invocations of functions provided by the model and the view model. 41 | For this purpose, this function knows the meaning of each message and each function. The dispatcher function is 42 | pure and does not maintain reference to the model and the view model. Instead, it receives current model and view 43 | model and returns new model and view model as in the following (abstract) signature: 44 | 45 | (Model, ViewModel) -> Message -> (Model, ViewModel, Effect) 46 | 47 | Depending on the exact message, the model may request that an effectful computation is performed. The dispatcher 48 | function does not execute effects. It returns them for execution elsewhere. *) 49 | 50 | let dispatch (m,vm) = function 51 | 52 | // User initiated action "add stamp to collection" in the view. 53 | | AddStamp (description, value) -> 54 | let m' = Model.addStampToCollection description value m 55 | let vm' = ViewModel.refresh m' vm 56 | (m',vm',None) 57 | 58 | // User initiated action "validate stamp rareness" in the view. 59 | | ValidateRareness (description, value) -> 60 | let validation = Effect <| Model.verifyStampRareness description value m 61 | // Transform the outcome of validation to a message which will be fed into this same dispatcher function. 62 | // Note that this means that Effect T becomes Effect Message with T carried by the corresponding message. 63 | let validation' = validation |> Effect.map RarenessValidated 64 | 65 | let vm' = ViewModel.rarenessVerificationInProgress vm 66 | 67 | (m,vm',validation') 68 | 69 | // The "effectful world" computed rareness of a stamp. 70 | | RarenessValidated (description, rareness) -> 71 | let m' = Model.setStampRareness description rareness m 72 | let vm' = vm |> ViewModel.refresh m' |> ViewModel.rarenessVerificationNotInProgress 73 | (m',vm',None) 74 | 75 | // ... and so on 76 | | SortStamps order -> 77 | // Note, that the model does not change, as this message is processed solely by the view model. 78 | let vm' = ViewModel.changeDisplayOrder m (Sorted order) vm 79 | (m,vm',None) 80 | 81 | | RemoveStampSorting -> 82 | // Note, that the model does not change, as this message is processed solely by the view model. 83 | let vm' = ViewModel.changeDisplayOrder m Unsorted vm 84 | (m,vm',None) -------------------------------------------------------------------------------- /Stamps/ViewModel/StampCollectionViewModel.fs: -------------------------------------------------------------------------------- 1 | namespace Stamps.ViewModel 2 | 3 | open System 4 | 5 | (* The view model of a stamp. It differs from the model of a stamp exactly because this is a model of the _view_. The 6 | first type is about rareness. Observe, that its terminology is a bit different: the view model does not care about the 7 | fact that rareness must be verified. We need similar values of rareness here for display purposes in the view. *) 8 | 9 | type Rareness = 10 | | NotDisplayed 11 | | Rare 12 | | NotRare 13 | 14 | override r.ToString () = 15 | match r with 16 | | NotDisplayed -> String.Empty 17 | | Rare -> "Rare" 18 | | NotRare -> "Common" 19 | 20 | (* The stamp view model proper contains basic stamp "properties" which should be displayed by the view. It is the 21 | intention, that the view provides possibility to determine rareness. This view model contains enabled/disabled state 22 | for the respective user interface action(s), e.g. buttons and menu commands. *) 23 | 24 | type StampViewModel = 25 | { 26 | Description : string 27 | Value : uint32 28 | Rareness : Rareness 29 | } 30 | 31 | member svm.IsVerificationEnabled = (svm.Rareness = NotDisplayed) 32 | 33 | override svm.ToString () = sprintf "%s (%d)" svm.Description svm.Value 34 | 35 | (* Here is an example of a display concern which should not be present in the model but only in the view model. Stamps 36 | can be displayed in certain order and the following type defines the possibilities. *) 37 | 38 | type StampSortedDisplayOrder = 39 | | ByValueAscending 40 | | ByValueDescending 41 | 42 | type StampDisplayOrder = 43 | | Unsorted // this will be interpreted as "in the order of the model" 44 | | Sorted of StampSortedDisplayOrder 45 | 46 | (* "The" view model is the list of stamp view models together with current display order. An invariant here is that 47 | the order of view models in the list corresponds to current display order value. *) 48 | 49 | type StampCollectionViewModel = 50 | { 51 | Stamps : List 52 | DisplayOrder : StampDisplayOrder 53 | IsRarenessVerificationInProgress : bool 54 | } 55 | 56 | module ViewModel = 57 | 58 | open Stamps.Model 59 | 60 | /// Some helpers. 61 | module private Aux = 62 | 63 | /// Sorts the stamps depending on stamp display order. 64 | let sortStamps (displayOrder : StampSortedDisplayOrder) = 65 | 66 | // the sorting relation 67 | let rel { StampViewModel.Value = v } = 68 | match displayOrder with 69 | | ByValueAscending -> int v 70 | | ByValueDescending -> -1 * (int v) 71 | 72 | List.sortBy rel 73 | 74 | /// Constructs the view model of one stamp. 75 | let viewModelOf (stamp : Stamp) : StampViewModel = 76 | 77 | let r = 78 | match stamp.Rareness with 79 | | VerifiedNotRare -> NotRare 80 | | VerifiedRare -> Rare 81 | | Unknown -> NotDisplayed 82 | 83 | { 84 | Description = stamp.Description 85 | Value = stamp.Value 86 | Rareness = r 87 | } 88 | 89 | /// Constructs a list of stamp view models from the complete model. 90 | let takeStampsFromModel = Model.allStamps >> List.map viewModelOf 91 | 92 | (* Public functions of the view model *) 93 | 94 | /// Computes the initial view model the application starts with, based on the initial model. 95 | let initialViewModel model = 96 | 97 | { 98 | Stamps = Aux.takeStampsFromModel model 99 | DisplayOrder = Unsorted 100 | IsRarenessVerificationInProgress = false 101 | } 102 | 103 | /// Refreshes the view model when the model changes either due to addition of a new stamp or change in stamp 104 | /// rareness. 105 | let refresh model (viewModel : StampCollectionViewModel) = 106 | 107 | let newStamps = Aux.takeStampsFromModel model 108 | 109 | (* As we are _refreshing_, retain current display order. *) 110 | let newStampsWithPreservedOrder = 111 | match viewModel.DisplayOrder with 112 | | Unsorted -> newStamps 113 | | Sorted order -> Aux.sortStamps order newStamps 114 | 115 | 116 | { viewModel with Stamps = newStampsWithPreservedOrder } 117 | 118 | /// Changes the display order of the stamps. 119 | let changeDisplayOrder model newDisplayOrder (viewModel : StampCollectionViewModel) = 120 | 121 | let newStamps = 122 | match viewModel.Stamps, newDisplayOrder with 123 | | currentStamps, _ when viewModel.DisplayOrder = newDisplayOrder -> currentStamps 124 | | currentStamps, Sorted order -> Aux.sortStamps order currentStamps 125 | | currentStamps, Unsorted -> Aux.takeStampsFromModel model 126 | 127 | { viewModel with Stamps = newStamps; DisplayOrder = newDisplayOrder } 128 | 129 | /// Makes the view model represent state when verification of stamp rareness is in progress. 130 | let rarenessVerificationInProgress (viewModel : StampCollectionViewModel) = 131 | { viewModel with IsRarenessVerificationInProgress = true } 132 | 133 | /// Makes the view model represent state when verification of stamp rareness is not in progress. 134 | let rarenessVerificationNotInProgress (viewModel : StampCollectionViewModel) = 135 | { viewModel with IsRarenessVerificationInProgress = false } -------------------------------------------------------------------------------- /Stamps/ViewModel/ViewModel.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | 2.0 8 | 7abe30de-3c24-48cc-b1bc-842cc83971f9 9 | Library 10 | ViewModel 11 | ViewModel 12 | v4.6.1 13 | 4.4.0.0 14 | true 15 | ViewModel 16 | 17 | 18 | true 19 | full 20 | false 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | 3 25 | bin\Debug\ViewModel.XML 26 | 27 | 28 | pdbonly 29 | true 30 | true 31 | bin\Release\ 32 | TRACE 33 | 3 34 | bin\Release\ViewModel.XML 35 | 36 | 37 | 38 | 39 | True 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | Model 53 | {148aee05-bb52-4a8b-a1cf-fbf707be52ec} 54 | True 55 | 56 | 57 | 58 | 11 59 | 60 | 61 | 62 | 63 | $(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets 64 | 65 | 66 | 67 | 68 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets 69 | 70 | 71 | 72 | 73 | 80 | --------------------------------------------------------------------------------