├── global.json ├── .config └── dotnet-tools.json ├── .editorconfig ├── .github └── workflows │ └── build.yml ├── src ├── Avalonia.Mvu │ ├── Avalonia.Mvu.fsproj │ └── Library.fs └── samples │ ├── CSharp │ ├── Program.cs │ ├── CSharp.csproj │ ├── CounterModule.cs │ └── MainView.cs │ └── FSharp │ ├── FSharp.fsproj │ └── Program.fs ├── .vscode ├── tasks.json └── launch.json ├── Avalonia.Mvu.sln ├── README.md └── .gitignore /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "7.0.100", 4 | "rollForward": "latestFeature" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "fantomas": { 6 | "version": "6.0.3", 7 | "commands": [ 8 | "fantomas" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.{fs,fsx}] 11 | max_line_length = 80 12 | fsharp_space_before_lowercase_invocation = false 13 | fsharp_multi_line_lambda_closing_newline = true 14 | fsharp_multiline_bracket_style = stroustrup 15 | fsharp_newline_before_multiline_computation_expression = false 16 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - release/* 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, windows-latest, macos-latest] 17 | name: Build ${{ matrix.os }} 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - uses: actions/checkout@v1 21 | - name: Setup .NET Core 22 | uses: actions/setup-dotnet@v1 23 | - name: Build Release 24 | run: dotnet build -c Release 25 | -------------------------------------------------------------------------------- /src/Avalonia.Mvu/Avalonia.Mvu.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;net6.0 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/samples/CSharp/Program.cs: -------------------------------------------------------------------------------- 1 | using Elmish; 2 | using static Avalonia.Mvu.CSharp; 3 | using CSharp; 4 | 5 | 6 | Window View() 7 | { 8 | Window(out var window) 9 | .Title("NXUI + Avalonia.MVU") 10 | .Width(300) 11 | .Height(300) 12 | .Content(MainView.Build(window)) 13 | #if DEBUG 14 | .AttachDevTools() 15 | #endif 16 | ; 17 | return window; 18 | } 19 | 20 | 21 | AppBuilder 22 | .Configure() 23 | .UsePlatformDetect() 24 | .UseFluentTheme(ThemeVariant.Dark) 25 | .WithApplicationName("NXUI + Avalonia.MVU") 26 | .StartWithClassicDesktopLifetime(View, args); 27 | -------------------------------------------------------------------------------- /src/samples/CSharp/CSharp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WinExe 5 | net7.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/samples/FSharp/FSharp.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WinExe 5 | net7.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build:fsharp", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/src/samples/FSharp/FSharp.fsproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "build:csharp", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "build", 22 | "${workspaceFolder}/src/samples/CSharp/CSharp.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "publish", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "publish", 34 | "${workspaceFolder}/src/Avalonia.Mvu/Avalonia.Mvu.fsproj", 35 | "/property:GenerateFullPaths=true", 36 | "/consoleloggerparameters:NoSummary" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch F#", 6 | "type": "coreclr", 7 | "request": "launch", 8 | "preLaunchTask": "build:fsharp", 9 | // If you have changed target frameworks, make sure to update the program path. 10 | "program": "${workspaceFolder}/src/samples/FSharp/bin/Debug/net7.0/FSharp.dll", 11 | "args": [], 12 | "cwd": "${workspaceFolder}/src/samples/FSharp", 13 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 14 | "console": "internalConsole", 15 | "stopAtEntry": false 16 | }, 17 | { 18 | "name": "Launch C#", 19 | "type": "coreclr", 20 | "request": "launch", 21 | "preLaunchTask": "build:csharp", 22 | // If you have changed target frameworks, make sure to update the program path. 23 | "program": "${workspaceFolder}/src/samples/CSharp/bin/Debug/net7.0/CSharp.dll", 24 | "args": [], 25 | "cwd": "${workspaceFolder}/src/samples/CSharp", 26 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 27 | "console": "internalConsole", 28 | "stopAtEntry": false 29 | }, 30 | { 31 | "name": ".NET Core Attach", 32 | "type": "coreclr", 33 | "request": "attach" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /Avalonia.Mvu.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{742A2B85-E56F-46D5-A920-9C6F43C360DC}" 7 | EndProject 8 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Avalonia.Mvu", "src\Avalonia.Mvu\Avalonia.Mvu.fsproj", "{2777A376-6F86-498C-8CD1-8345D81EDE9E}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{80E5F0AA-DEEF-4AF7-B2AE-4C48873B4CC3}" 11 | EndProject 12 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp", "src\samples\FSharp\FSharp.fsproj", "{21B808A0-A381-4F00-9956-9A73A3D2AEE1}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharp", "src\samples\CSharp\CSharp.csproj", "{2A043FCF-F500-40DD-AAE5-984D6E8A1DE0}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(SolutionProperties) = preSolution 22 | HideSolutionNode = FALSE 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {2777A376-6F86-498C-8CD1-8345D81EDE9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {2777A376-6F86-498C-8CD1-8345D81EDE9E}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {2777A376-6F86-498C-8CD1-8345D81EDE9E}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {2777A376-6F86-498C-8CD1-8345D81EDE9E}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {21B808A0-A381-4F00-9956-9A73A3D2AEE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {21B808A0-A381-4F00-9956-9A73A3D2AEE1}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {21B808A0-A381-4F00-9956-9A73A3D2AEE1}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {21B808A0-A381-4F00-9956-9A73A3D2AEE1}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {2A043FCF-F500-40DD-AAE5-984D6E8A1DE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {2A043FCF-F500-40DD-AAE5-984D6E8A1DE0}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {2A043FCF-F500-40DD-AAE5-984D6E8A1DE0}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {2A043FCF-F500-40DD-AAE5-984D6E8A1DE0}.Release|Any CPU.Build.0 = Release|Any CPU 37 | EndGlobalSection 38 | GlobalSection(NestedProjects) = preSolution 39 | {2777A376-6F86-498C-8CD1-8345D81EDE9E} = {742A2B85-E56F-46D5-A920-9C6F43C360DC} 40 | {80E5F0AA-DEEF-4AF7-B2AE-4C48873B4CC3} = {742A2B85-E56F-46D5-A920-9C6F43C360DC} 41 | {21B808A0-A381-4F00-9956-9A73A3D2AEE1} = {80E5F0AA-DEEF-4AF7-B2AE-4C48873B4CC3} 42 | {2A043FCF-F500-40DD-AAE5-984D6E8A1DE0} = {80E5F0AA-DEEF-4AF7-B2AE-4C48873B4CC3} 43 | EndGlobalSection 44 | EndGlobal 45 | -------------------------------------------------------------------------------- /src/samples/FSharp/Program.fs: -------------------------------------------------------------------------------- 1 | open System 2 | 3 | open Avalonia 4 | open Avalonia.Data 5 | open Avalonia.Controls 6 | 7 | open NXUI.Extensions 8 | open NXUI.FSharp.Extensions 9 | 10 | open Elmish 11 | open Avalonia.Mvu 12 | 13 | open FSharp.Control.Reactive 14 | 15 | type State = { 16 | count: int 17 | name: string 18 | nameFound: bool 19 | } 20 | 21 | type Message = 22 | | Increment 23 | | Decrement 24 | | Reset 25 | | NameFound of bool 26 | | SetName of string 27 | 28 | let panelContent (window: Window) : StackPanel = 29 | 30 | let counter, dispatch = 31 | useElmish( 32 | { 33 | count = 0 34 | name = "" 35 | nameFound = false 36 | }, 37 | fun msg state -> 38 | match msg with 39 | | Increment -> 40 | { state with count = state.count + 1 }, Cmd.ofMsg(SetName "Increment") 41 | | Decrement -> 42 | { state with count = state.count - 1 }, Cmd.ofMsg(SetName "Decrement") 43 | | Reset -> { state with count = 0 }, Cmd.ofMsg(SetName "Reset") 44 | | NameFound found -> { state with nameFound = found }, Cmd.none 45 | | SetName name -> 46 | let wasNameFound (name: string) = 47 | name.Equals("peter", StringComparison.InvariantCultureIgnoreCase) 48 | 49 | let ofSuccess (found: bool) = NameFound found 50 | 51 | { state with name = name }, 52 | Cmd.OfFunc.perform wasNameFound name ofSuccess 53 | ) 54 | 55 | let counterText = 56 | counter 57 | |> Observable.map(fun ({ count = count }) -> count) 58 | |> Observable.distinctUntilChanged 59 | |> Observable.map(fun count -> $"You clicked %i{count} times") 60 | 61 | let nameFound = 62 | counter 63 | |> Observable.map(fun ({ nameFound = nameFound }) -> nameFound) 64 | |> Observable.distinctUntilChanged 65 | 66 | let actionPerformed = 67 | counter 68 | |> Observable.map(fun ({ name = name }) -> name) 69 | |> Observable.distinctUntilChanged 70 | |> Observable.map(fun name -> $"Action Performed: {name}") 71 | 72 | let incrementOnClick _ observable = 73 | observable |> Observable.add(fun _ -> dispatch Increment) 74 | 75 | let checkText _ observable = 76 | observable 77 | |> Observable.throttle(TimeSpan.FromMilliseconds(250)) 78 | |> Observable.add(fun name -> dispatch(SetName name)) 79 | 80 | StackPanel() 81 | .children( 82 | Button().content("Click me!!").OnClick(incrementOnClick), 83 | TextBox().text(window.BindTitle()), 84 | TextBlock().text(counterText, BindingMode.OneWay), 85 | TextBlock().text(actionPerformed, BindingMode.OneWay), 86 | StackPanel() 87 | .spacing(4.0) 88 | .children( 89 | Label().content("Find the name!"), 90 | TextBox().watermark("Starts with P ends with R").OnText(checkText) 91 | ), 92 | CheckBox() 93 | .isChecked(nameFound.ToBinding(), BindingMode.OneWay) 94 | .isEnabled(false) 95 | .content("Name Found") 96 | ) 97 | 98 | let view () : Window = 99 | let window = Window().title("NXUI and F#").width(300).height(300) 100 | #if DEBUG 101 | window.AttachDevTools() 102 | #endif 103 | window.content(panelContent window) 104 | 105 | [] 106 | let main argv = 107 | AppBuilder 108 | .Configure() 109 | .UsePlatformDetect() 110 | .UseFluentTheme(Styling.ThemeVariant.Dark) 111 | .WithApplicationName("NXUI and F#") 112 | .StartWithClassicDesktopLifetime(view, argv) 113 | -------------------------------------------------------------------------------- /src/samples/CSharp/CounterModule.cs: -------------------------------------------------------------------------------- 1 | namespace CSharp; 2 | 3 | using Elmish; 4 | using static Avalonia.Mvu.CSharp; 5 | 6 | /// 7 | /// This static class contains an Elmish View that matches the MVU paradigm. 8 | /// T 9 | /// 10 | public static class CounterModule 11 | { 12 | /// 13 | /// The M in MVU this represents the model or also called state 14 | /// that contains the data of the current view and it is used to populate it. 15 | /// 16 | /// Represents internal data, in this case an integer for a counter 17 | private sealed record Model(int Count = 0, string Label = "Type Something :)"); 18 | 19 | /// 20 | /// As there are no Discriminated Unions in C# we can make a record that contains 21 | /// our "cases" for the messages that we want to send to the update function. 22 | /// Each message represents an action in the view that will be scheduled to be executed in the update function. 23 | /// 24 | private abstract record Message 25 | { 26 | public sealed record Increment : Message; 27 | public sealed record Decrement : Message; 28 | 29 | public sealed record SetText(string Text) : Message; 30 | } 31 | 32 | /// 33 | /// This build method prepares our MVU based control to be used as part of any other 34 | /// Avalonia application. 35 | /// 36 | /// Our Built UI based on the current state of our MVU 37 | public static Control Build() => 38 | UseElmishView( 39 | // Model is the initial state of our MVU 40 | new Model(10), 41 | // Update is the function that will be called when a message is dispatched 42 | // and it will return a new model and a command to be executed. 43 | (message, model) => 44 | message switch 45 | { 46 | Message.Increment => 47 | (model with { Count = model.Count + 1 }, Cmd.none()), 48 | Message.Decrement => 49 | (model with { Count = model.Count - 1 }, Cmd.none()), 50 | Message.SetText(var text) => 51 | (model with { Label = text }, Cmd.none()), 52 | _ => (model, Cmd.none()), 53 | }, 54 | // View is the function that will be called to build the UI based on the current state of the MVU 55 | // and the dispatch function that will be used to send messages to the update function. 56 | (model, dispatch) => 57 | StackPanel() 58 | .Spacing(4.0) 59 | .Children( 60 | Button() 61 | .Content("Increment").OnClick((_, o) => 62 | // we'll observe clicks on the button and dispatch an increment message 63 | o.Subscribe(_ => dispatch(new Message.Increment()))), 64 | // We'll use the current state of the model where we would normally "bind" the text 65 | TextBlock().Text($"You clicked {model.Count} times"), 66 | StackPanel() 67 | .Spacing(4.0) 68 | .Children( 69 | Label().Content(model.Label), 70 | // currently screen state (not mvu state but what's on the screen) is lost as any change in the state 71 | // re-renders the whole thing, if I can get it fixed I'll publish this otherwise, It was a good project 72 | // to experiment with :) 73 | TextBox() 74 | .Text(model.Label) 75 | .OnText((_, o) => 76 | { 77 | o.Subscribe(text => dispatch(new Message.SetText(text))); 78 | }) 79 | ), 80 | // dynamic content is also supported 81 | model.Count == 20 82 | ? TextBlock().Text("You have reached 20!") 83 | : TextBlock().Text("Not reached 20 yet!"), 84 | Button() 85 | .Content("Decrement").OnClick((_, o) => 86 | // we'll observe clicks on the button and dispatch an decrement message 87 | o.Subscribe(_ => dispatch(new Message.Decrement()))) 88 | ) 89 | ); 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/samples/CSharp/MainView.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.FSharp.Collections; 2 | using Microsoft.FSharp.Core; 3 | 4 | namespace CSharp; 5 | 6 | using Elmish; 7 | using static Avalonia.Mvu.CSharp; 8 | 9 | public static class MainView 10 | { 11 | private sealed record State(int Count = 0, string Name = "", bool NameFound = false); 12 | 13 | private abstract record Message 14 | { 15 | public sealed record Increment : Message; 16 | 17 | public sealed record Decrement : Message; 18 | 19 | public sealed record Reset : Message; 20 | 21 | public sealed record NameFound(bool Found) : Message; 22 | 23 | public sealed record SetName(string Name) : Message; 24 | } 25 | 26 | /// The only thing I don't like about this are the F# command signatures. but I guess that's the price to pay for 27 | /// being able to use MVU in C#. I'm not sure if there's a way to make this more sensible. 28 | /// Maybe in .NET8 that has a broader support for Type Aliasing 29 | private static (State, FSharpList, Unit>> cmd) UpdateSetname(State state, string name) 30 | { 31 | var wasNameFound = (string maybeName) => maybeName.Equals("peter", StringComparison.InvariantCultureIgnoreCase); 32 | 33 | Func onSuccess = nameFound => new Message.NameFound(nameFound); 34 | 35 | var cmd = Cmdcs.OfFunc.perform(wasNameFound, name, onSuccess); 36 | 37 | return (state with { Name = name }, cmd); 38 | } 39 | 40 | public static Control Build(Window window) 41 | { 42 | var (counter, dispatch) = 43 | UseElmish(model: new State(Count: 0), update: (message, state) => 44 | { 45 | return message switch 46 | { 47 | Message.Increment => 48 | (state with { Count = state.Count + 1 }, Cmd.ofMsg(new Message.SetName("Increment"))), 49 | Message.Decrement => 50 | (state with { Count = state.Count - 1 }, Cmd.ofMsg(new Message.SetName("Decrement"))), 51 | Message.Reset => 52 | (state with { Count = 0 }, Cmd.ofMsg(new Message.SetName("Reset"))), 53 | Message.NameFound(var nameFound) => 54 | (state with { NameFound = nameFound }, Cmd.none()), 55 | // for cases that require more than just an expression 56 | // you can always fallback to a method within the module 57 | Message.SetName({ } name) => UpdateSetname(state, name), 58 | _ => (state, Cmd.none()) 59 | }; 60 | }); 61 | 62 | var counterText = 63 | counter 64 | .Select(state => state.Count) 65 | .DistinctUntilChanged() 66 | .Select(count => $"You clicked {count} times"); 67 | 68 | var nameFound = 69 | counter 70 | .Select(state => state.NameFound) 71 | .DistinctUntilChanged(); 72 | 73 | var actionPerformed = counter 74 | .Select(state => state.Name) 75 | .DistinctUntilChanged() 76 | .Select(name => $"Action Performed: {name}"); 77 | 78 | void IncrementOnClick(Button sender, IObservable e) => 79 | e.Subscribe(_ => dispatch(new Message.Increment())); 80 | 81 | void CheckText(TextBox sender, IObservable e) => 82 | e 83 | .Throttle(TimeSpan.FromMilliseconds(250)) 84 | .Subscribe(name => dispatch(new Message.SetName(name))); 85 | 86 | return StackPanel() 87 | .Children( 88 | Button().Content("Click me!").OnClick(IncrementOnClick), 89 | TextBox().Text(window.BindTitle()), 90 | TextBlock().Text(counterText, BindingMode.OneWay), 91 | TextBlock().Text(actionPerformed, BindingMode.OneWay), 92 | StackPanel() 93 | .Spacing(4.0) 94 | .Children( 95 | Label().Content("Find the name!"), 96 | TextBox().Watermark("Starts with P ends with R").OnText(CheckText) 97 | ), 98 | CheckBox() 99 | .IsChecked(nameFound.ToBinding(), BindingMode.OneWay) 100 | .IsEnabled(false) 101 | .Content("Name Found"), 102 | StackPanel() 103 | .Margin(new Thickness(10)) 104 | .Children( 105 | Label().Content("Elmish Counter"), 106 | CounterModule.Build() 107 | ) 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Avalonia.FuncUI]: https://github.com/fsprojects/Avalonia.FuncUI 2 | [NXUI]: https://github.com/wieslawsoltes/NXUI 3 | [Fabulous.Avalonia]: https://github.com/fabulous-dev/Fabulous.Avalonia 4 | [Elmish.Avalonia]: https://github.com/JordanMarr/Elmish.Avalonia 5 | 6 | ## Avalonia.MVU 7 | 8 | Avalonia.MVU is a Model-View-Update (MVU) library for Avalonia. 9 | 10 | This library works for both C# and F# and is best used with [NXUI] and probably plain avalonia code not markup. 11 | 12 | ## Existing Alternatives 13 | 14 | Frankly speaking, there are already some MVU libraries for Avalonia and they are better for you to try out first. 15 | 16 | - [Avalonia.FuncUI] - A true MVU plus react-like library for Avalonia. If you have a hint of react/elm then this is for you. 17 | - [Fabulous.Avalonia] - Fabulous supports a wide range of backends not just Avalonia but they are MVU first. 18 | - [Elmish.Avalonia] - Elmish Avalonia is a port of Elmish.WPF for Avalonia, you can use XAML to define your views and use MVU for the rest. It also supports Design View in VS2022 or Rider. 19 | 20 | ## Call For Help! 21 | 22 | Do you truly want to see MVU for C# in Avalonia? please help me out! 23 | 24 | The current code is in [The Library.fs File](./src/Avalonia.Mvu/Library.fs) and you can look after `UseElmishView` function. If you can help me out to make it work then we're good to go full MVU in C# not just F#. 25 | 26 | ### Work In Progress 27 | 28 | This library is still work in progress and is not yet ready for production use. 29 | 30 | While library mentions MVU, it is not a strict MVU implementation (yet I hope). 31 | 32 | The primary intended use case for this library is to allow MVU-like updates in Avalonia but using observables to update the UI. 33 | 34 | Here's an example: 35 | 36 | ```csharp 37 | 38 | public static class MainViewModule 39 | { 40 | private sealed record State(int Count = 0); 41 | 42 | private abstract record Message 43 | { 44 | public sealed record Increment : Message; 45 | 46 | public sealed record Decrement : Message; 47 | } 48 | 49 | 50 | public static Control Build(Window window) 51 | { 52 | var (state, dispatch) = 53 | UseElmish(model: new State(), update: (message, state) => 54 | { 55 | return message switch 56 | { 57 | Message.Increment => 58 | (state with { Count = state.Count + 1 }, Cmd.ofMsg(new Message.SetName("Increment"))), 59 | Message.Decrement => 60 | (state with { Count = state.Count - 1 }, Cmd.ofMsg(new Message.SetName("Decrement"))), 61 | _ => (state, Cmd.none()) 62 | }; 63 | }); 64 | 65 | var counterText = 66 | state 67 | .Select(state => state.Count) 68 | .DistinctUntilChanged() 69 | .Select(count => $"You clicked {count} times"); 70 | 71 | 72 | void IncrementOnClick(Button sender, IObservable e) => 73 | e.Subscribe(_ => dispatch(new Message.Increment())); 74 | 75 | void DecrementOnClick(Button sender, IObservable e) => 76 | e.Subscribe(_ => dispatch(new Message.Decrement())); 77 | 78 | return StackPanel() 79 | .Spacing(4.0) 80 | .Children( 81 | Button().Content("Increment").OnClick(IncrementOnClick), 82 | TextBlock().Text(counterText, BindingMode.OneWay), 83 | Button().Content("Decrement").OnClick(IncrementOnClick), 84 | ); 85 | } 86 | } 87 | ``` 88 | 89 | This is more of a MU than MVU where the model and update part is MVU but the view part is not. I personally think this is kind of the best of both worlds as you're not forced to fall within the MVU paradigm for the view part so you can freely interop with many of avalonia controls, even those made from thir party developers, you're still in control of a predictable state -> update -> view control flow using observables and making them bindings for your avalonia code, this is also performant as we're not re-rendering the whole view on partial state updates (e.g. update the text property in the model would mean to re-render the whole view or diff to patch the view) but rather controlling the view updates using observables. 90 | 91 | --- 92 | 93 | #### For those who want full MVU: 94 | 95 | I understand this may not be the MVU some people are looking after but doing the View part has it's own challenges, and that's why the MVU alternatives are front and center at the top of this readme, unfortunately for the C# devs those alternatives provided are F# ones I personally don't think that should be a show stopper as both are .NET langauges and frankly F# is a better suited language for an MVU paradigm, but I do understand that some people may not want to learn F# and that's fine, I'm not here to force you to learn F# but I do want to help you out to get MVU in C#. 96 | 97 | However I did try to make it happen, ideally we want to also include the View part here so that it is a complete example and you can see that in [The Counter Module](./src/samples/CSharp/CounterModule.cs) but if you have ideas to be able to make it work let's make them a reality!. 98 | -------------------------------------------------------------------------------- /src/Avalonia.Mvu/Library.fs: -------------------------------------------------------------------------------- 1 | module Avalonia.Mvu 2 | 3 | open System 4 | open Elmish 5 | open System.Reactive.Subjects 6 | open Avalonia 7 | open Avalonia.Controls 8 | open Avalonia.Controls.Templates 9 | open Avalonia.Data 10 | open Avalonia.Threading 11 | 12 | 13 | /// Starts an Elmish loop and provides a Dispatch method that calls the given setModel fn. 14 | type internal ElmishState<'model, 'msg, 'arg> 15 | (mkProgram: unit -> Program<'arg, 'model, 'msg, unit>, arg: 'arg, setModel) = 16 | let program = mkProgram() 17 | 18 | let mutable _model = Program.init program arg |> fst 19 | let mutable _dispatch = fun (_: 'msg) -> () 20 | 21 | let setState model dispatch = 22 | _dispatch <- dispatch 23 | 24 | // This shouldn't be an issue as the models are meant to be records which come 25 | // with *free equality guarantees* Of course I may be wrong :P 26 | if not(obj.ReferenceEquals(model, _model)) then 27 | _model <- model 28 | setModel model 29 | 30 | // Syncs view changes from non-UI threads through the Avalonia dispatcher. 31 | let syncDispatch (dispatch: Dispatch<'msg>) (msg: 'msg) = 32 | if 33 | Dispatcher.UIThread.CheckAccess() // Is this already on the UI thread? 34 | then 35 | dispatch msg 36 | else 37 | Dispatcher.UIThread.Post(fun () -> dispatch msg) 38 | 39 | do 40 | program 41 | |> Program.withSetState setState 42 | |> Program.runWithDispatch syncDispatch arg 43 | 44 | member _.Model = _model 45 | member _.Dispatch = _dispatch 46 | 47 | 48 | /// C# Heloper module to provide Func/Action based versions of Elmish Cmd modules. 49 | /// This module won't change the Elmish Cmd module, but instead provides a C#-friendly 50 | /// also those Cmds that can work relatively nice in C# won't be added here 51 | module CSharp = 52 | 53 | /// 54 | /// Provides a C#-friendly version of the Elmish Cmd module. 55 | /// 56 | module Cmdcs = 57 | let inline map (f: Func<'Value, 'Message>, cmd: Cmd<'Value>) = 58 | Cmd.map (FuncConvert.FromFunc f) cmd 59 | 60 | module OfFunc = 61 | let either 62 | ( 63 | fn: Func<'Arg, 'Return>, 64 | arg: 'Arg, 65 | ofSuccess: Func<'Return, 'Message>, 66 | ofError: Func 67 | ) : Cmd<'Message> = 68 | let fn = FuncConvert.FromFunc fn 69 | let onSuccess = FuncConvert.FromFunc ofSuccess 70 | let onError = FuncConvert.FromFunc ofError 71 | Cmd.OfFunc.either fn arg onSuccess onError 72 | 73 | let perform 74 | ( 75 | fn: Func<'Arg, 'Return>, 76 | arg: 'Arg, 77 | ofSuccess: Func<'Return, 'Message> 78 | ) = 79 | let fn = FuncConvert.FromFunc fn 80 | let onSuccess = FuncConvert.FromFunc ofSuccess 81 | Cmd.OfFunc.perform fn arg onSuccess 82 | 83 | let attempt (fn: Action<'Arg>, arg: 'Arg, ofError: Func) = 84 | let fn = FuncConvert.FromAction fn 85 | let onError = FuncConvert.FromFunc ofError 86 | Cmd.OfFunc.attempt fn arg onError 87 | 88 | module OfTask = 89 | open System.Threading.Tasks 90 | 91 | let either 92 | ( 93 | fn: Func<'Arg, Task<'Return>>, 94 | arg: 'Arg, 95 | ofSuccess: Func<'Return, 'Message>, 96 | ofError: Func 97 | ) = 98 | let fn = FuncConvert.FromFunc fn 99 | let onSuccess = FuncConvert.FromFunc ofSuccess 100 | let onError = FuncConvert.FromFunc ofError 101 | Cmd.OfTask.either fn arg onSuccess onError 102 | 103 | let perform 104 | ( 105 | fn: Func<'Arg, Task<'Return>>, 106 | arg: 'Arg, 107 | ofSuccess: Func<'Return, 'Message> 108 | ) = 109 | let fn = FuncConvert.FromFunc fn 110 | let onSuccess = FuncConvert.FromFunc ofSuccess 111 | Cmd.OfTask.perform fn arg onSuccess 112 | 113 | let attempt 114 | ( 115 | fn: Func<'Arg, #Task>, 116 | arg: 'Arg, 117 | ofError: Func 118 | ) = 119 | let fn = FuncConvert.FromFunc fn 120 | let onError = FuncConvert.FromFunc ofError 121 | Cmd.OfTask.attempt fn arg onError 122 | 123 | /// 124 | /// Creates an Elmish loop and returns the model observable and a dispatch function. 125 | /// 126 | /// The initial model. 127 | /// The update function that takes messages and updates the state accordingly. 128 | /// A tuple containing the model observable and a dispatch function. 129 | let UseElmish 130 | ( 131 | model: 'Model, 132 | update: Func<'Msg, 'Model, struct ('Model * Cmd<'Msg>)> 133 | ) : ValueTuple, Action<'Msg>> = 134 | 135 | let ignoreView = (fun _ _ -> ()) 136 | 137 | let mkProgram () = 138 | let init () = model, Cmd.none 139 | 140 | let update = 141 | let inline update msg model = 142 | let struct (model, msg) = update.Invoke(msg, model) 143 | (model, msg) 144 | 145 | update 146 | 147 | Program.mkProgram init update ignoreView 148 | 149 | let _model = new BehaviorSubject<'Model>(model) 150 | 151 | let state = ElmishState(mkProgram, (), _model.OnNext) 152 | 153 | let dispatch map = state.Dispatch map 154 | 155 | struct (_model :> IObservable<'Model>, Action<_>(dispatch)) 156 | 157 | /// 158 | /// Creates an Elmish/MVU loop and returns a ContentControl that renders the view. 159 | /// 160 | /// The initial model. 161 | /// The update function that takes messages and updates the state accordingly. 162 | /// The view function that takes the model and a dispatch function and returns a Control. 163 | /// A ContentControl that renders the view. 164 | [] 165 | let UseElmishView 166 | ( 167 | model: 'Model, 168 | update: Func<'Msg, 'Model, struct ('Model * Cmd<'Msg>)>, 169 | view: Func<'Model, Action<'Msg>, Control> 170 | ) = 171 | 172 | let struct (model, dispatch) = UseElmish(model, update) 173 | 174 | let content = 175 | ContentControl( 176 | ContentTemplate = 177 | // currently we're just re-rendering the whole thing 178 | // which brings problems with focus, textbox view state and similar 179 | // ideally if we could replace this FuncDataTemplate with a DataTemplate 180 | // that supports recycling to avoid re-rendering the whole thing 181 | // it would be ideal, though I don't personally know how to make that possible 182 | FuncDataTemplate<'Model>(fun model _ -> view.Invoke(model, dispatch)) 183 | ) 184 | 185 | let descriptor = 186 | ContentControl.ContentProperty.Bind().WithMode(BindingMode.OneWay) 187 | 188 | content[descriptor] <- model.ToBinding() 189 | content 190 | 191 | /// 192 | /// Creates an Elmish loop and returns the model observable and a dispatch function. 193 | /// 194 | /// 195 | /// This function is meant to be consumed from F# code. 196 | /// For the C# code please check UseElmish. 197 | /// 198 | /// The initial model. 199 | /// The update function that takes messages and updates the state accordingly. 200 | /// A tuple containing the model observable and a dispatch function. 201 | let useElmish 202 | ( 203 | model: 'Model, 204 | update: 'Msg -> 'Model -> 'Model * Cmd<'Msg> 205 | ) : IObservable<'Model> * Dispatch<'Msg> = 206 | let ignoreView = (fun _ _ -> ()) 207 | 208 | let mkProgram () = 209 | let init () = model, Cmd.none 210 | 211 | Program.mkProgram init update ignoreView 212 | 213 | let _model = new BehaviorSubject<'Model>(model) 214 | 215 | let state = ElmishState(mkProgram, (), _model.OnNext) 216 | 217 | let dispatch map = state.Dispatch map 218 | 219 | _model :> IObservable<'Model>, dispatch 220 | 221 | /// 222 | /// Creates an Elmish/MVU loop and returns a ContentControl that renders the view. 223 | /// 224 | /// 225 | /// This function is meant to be consumed from F# code. 226 | /// For the C# code please check UseElmishView. 227 | /// 228 | /// The initial model. 229 | /// The update function that takes messages and updates the state accordingly. 230 | /// The view function that takes the model and a dispatch function and returns a Control. 231 | /// A ContentControl that renders the view. 232 | [] 233 | let useElmishView 234 | ( 235 | model: 'Model, 236 | update: 'Msg -> 'Model -> 'Model * Cmd<'Msg>, 237 | view: 'Model -> ('Msg -> unit) -> Control 238 | ) = 239 | let model, dispatch = useElmish(model, update) 240 | 241 | let content = 242 | ContentControl( 243 | ContentTemplate = 244 | // currently we're just re-rendering the whole thing 245 | // which brings problems with focus, textbox view state and similar 246 | // ideally if we could replace this FuncDataTemplate with a DataTemplate 247 | // that supports recycling to avoid re-rendering the whole thing 248 | // it would be ideal, though I don't personally know how to make that possible 249 | FuncDataTemplate<'Model>(fun model _ -> view model dispatch) 250 | ) 251 | 252 | let descriptor = 253 | ContentControl.ContentProperty.Bind().WithMode(BindingMode.OneWay) 254 | 255 | content[descriptor] <- model.ToBinding() 256 | 257 | content 258 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # Tye 66 | .tye/ 67 | 68 | # ASP.NET Scaffolding 69 | ScaffoldingReadMe.txt 70 | 71 | # StyleCop 72 | StyleCopReport.xml 73 | 74 | # Files built by Visual Studio 75 | *_i.c 76 | *_p.c 77 | *_h.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.iobj 82 | *.pch 83 | *.pdb 84 | *.ipdb 85 | *.pgc 86 | *.pgd 87 | *.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.tlog 97 | *.vspscc 98 | *.vssscc 99 | .builds 100 | *.pidb 101 | *.svclog 102 | *.scc 103 | 104 | # Chutzpah Test files 105 | _Chutzpah* 106 | 107 | # Visual C++ cache files 108 | ipch/ 109 | *.aps 110 | *.ncb 111 | *.opendb 112 | *.opensdf 113 | *.sdf 114 | *.cachefile 115 | *.VC.db 116 | *.VC.VC.opendb 117 | 118 | # Visual Studio profiler 119 | *.psess 120 | *.vsp 121 | *.vspx 122 | *.sap 123 | 124 | # Visual Studio Trace Files 125 | *.e2e 126 | 127 | # TFS 2012 Local Workspace 128 | $tf/ 129 | 130 | # Guidance Automation Toolkit 131 | *.gpState 132 | 133 | # ReSharper is a .NET coding add-in 134 | _ReSharper*/ 135 | *.[Rr]e[Ss]harper 136 | *.DotSettings.user 137 | 138 | # TeamCity is a build add-in 139 | _TeamCity* 140 | 141 | # DotCover is a Code Coverage Tool 142 | *.dotCover 143 | 144 | # AxoCover is a Code Coverage Tool 145 | .axoCover/* 146 | !.axoCover/settings.json 147 | 148 | # Coverlet is a free, cross platform Code Coverage Tool 149 | coverage*.json 150 | coverage*.xml 151 | coverage*.info 152 | 153 | # Visual Studio code coverage results 154 | *.coverage 155 | *.coveragexml 156 | 157 | # NCrunch 158 | _NCrunch_* 159 | .*crunch*.local.xml 160 | nCrunchTemp_* 161 | 162 | # MightyMoose 163 | *.mm.* 164 | AutoTest.Net/ 165 | 166 | # Web workbench (sass) 167 | .sass-cache/ 168 | 169 | # Installshield output folder 170 | [Ee]xpress/ 171 | 172 | # DocProject is a documentation generator add-in 173 | DocProject/buildhelp/ 174 | DocProject/Help/*.HxT 175 | DocProject/Help/*.HxC 176 | DocProject/Help/*.hhc 177 | DocProject/Help/*.hhk 178 | DocProject/Help/*.hhp 179 | DocProject/Help/Html2 180 | DocProject/Help/html 181 | 182 | # Click-Once directory 183 | publish/ 184 | 185 | # Publish Web Output 186 | *.[Pp]ublish.xml 187 | *.azurePubxml 188 | # Note: Comment the next line if you want to checkin your web deploy settings, 189 | # but database connection strings (with potential passwords) will be unencrypted 190 | *.pubxml 191 | *.publishproj 192 | 193 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 194 | # checkin your Azure Web App publish settings, but sensitive information contained 195 | # in these scripts will be unencrypted 196 | PublishScripts/ 197 | 198 | # NuGet Packages 199 | *.nupkg 200 | # NuGet Symbol Packages 201 | *.snupkg 202 | # The packages folder can be ignored because of Package Restore 203 | **/[Pp]ackages/* 204 | # except build/, which is used as an MSBuild target. 205 | !**/[Pp]ackages/build/ 206 | # Uncomment if necessary however generally it will be regenerated when needed 207 | #!**/[Pp]ackages/repositories.config 208 | # NuGet v3's project.json files produces more ignorable files 209 | *.nuget.props 210 | *.nuget.targets 211 | 212 | # Microsoft Azure Build Output 213 | csx/ 214 | *.build.csdef 215 | 216 | # Microsoft Azure Emulator 217 | ecf/ 218 | rcf/ 219 | 220 | # Windows Store app package directories and files 221 | AppPackages/ 222 | BundleArtifacts/ 223 | Package.StoreAssociation.xml 224 | _pkginfo.txt 225 | *.appx 226 | *.appxbundle 227 | *.appxupload 228 | 229 | # Visual Studio cache files 230 | # files ending in .cache can be ignored 231 | *.[Cc]ache 232 | # but keep track of directories ending in .cache 233 | !?*.[Cc]ache/ 234 | 235 | # Others 236 | ClientBin/ 237 | ~$* 238 | *~ 239 | *.dbmdl 240 | *.dbproj.schemaview 241 | *.jfm 242 | *.pfx 243 | *.publishsettings 244 | orleans.codegen.cs 245 | 246 | # Including strong name files can present a security risk 247 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 248 | #*.snk 249 | 250 | # Since there are multiple workflows, uncomment next line to ignore bower_components 251 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 252 | #bower_components/ 253 | 254 | # RIA/Silverlight projects 255 | Generated_Code/ 256 | 257 | # Backup & report files from converting an old project file 258 | # to a newer Visual Studio version. Backup files are not needed, 259 | # because we have git ;-) 260 | _UpgradeReport_Files/ 261 | Backup*/ 262 | UpgradeLog*.XML 263 | UpgradeLog*.htm 264 | ServiceFabricBackup/ 265 | *.rptproj.bak 266 | 267 | # SQL Server files 268 | *.mdf 269 | *.ldf 270 | *.ndf 271 | 272 | # Business Intelligence projects 273 | *.rdl.data 274 | *.bim.layout 275 | *.bim_*.settings 276 | *.rptproj.rsuser 277 | *- [Bb]ackup.rdl 278 | *- [Bb]ackup ([0-9]).rdl 279 | *- [Bb]ackup ([0-9][0-9]).rdl 280 | 281 | # Microsoft Fakes 282 | FakesAssemblies/ 283 | 284 | # GhostDoc plugin setting file 285 | *.GhostDoc.xml 286 | 287 | # Node.js Tools for Visual Studio 288 | .ntvs_analysis.dat 289 | node_modules/ 290 | 291 | # Visual Studio 6 build log 292 | *.plg 293 | 294 | # Visual Studio 6 workspace options file 295 | *.opt 296 | 297 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 298 | *.vbw 299 | 300 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 301 | *.vbp 302 | 303 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 304 | *.dsw 305 | *.dsp 306 | 307 | # Visual Studio 6 technical files 308 | *.ncb 309 | *.aps 310 | 311 | # Visual Studio LightSwitch build output 312 | **/*.HTMLClient/GeneratedArtifacts 313 | **/*.DesktopClient/GeneratedArtifacts 314 | **/*.DesktopClient/ModelManifest.xml 315 | **/*.Server/GeneratedArtifacts 316 | **/*.Server/ModelManifest.xml 317 | _Pvt_Extensions 318 | 319 | # Paket dependency manager 320 | .paket/paket.exe 321 | paket-files/ 322 | 323 | # FAKE - F# Make 324 | .fake/ 325 | 326 | # CodeRush personal settings 327 | .cr/personal 328 | 329 | # Python Tools for Visual Studio (PTVS) 330 | __pycache__/ 331 | *.pyc 332 | 333 | # Cake - Uncomment if you are using it 334 | # tools/** 335 | # !tools/packages.config 336 | 337 | # Tabs Studio 338 | *.tss 339 | 340 | # Telerik's JustMock configuration file 341 | *.jmconfig 342 | 343 | # BizTalk build output 344 | *.btp.cs 345 | *.btm.cs 346 | *.odx.cs 347 | *.xsd.cs 348 | 349 | # OpenCover UI analysis results 350 | OpenCover/ 351 | 352 | # Azure Stream Analytics local run output 353 | ASALocalRun/ 354 | 355 | # MSBuild Binary and Structured Log 356 | *.binlog 357 | 358 | # NVidia Nsight GPU debugger configuration file 359 | *.nvuser 360 | 361 | # MFractors (Xamarin productivity tool) working folder 362 | .mfractor/ 363 | 364 | # Local History for Visual Studio 365 | .localhistory/ 366 | 367 | # Visual Studio History (VSHistory) files 368 | .vshistory/ 369 | 370 | # BeatPulse healthcheck temp database 371 | healthchecksdb 372 | 373 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 374 | MigrationBackup/ 375 | 376 | # Ionide (cross platform F# VS Code tools) working folder 377 | .ionide/ 378 | 379 | # Fody - auto-generated XML schema 380 | FodyWeavers.xsd 381 | 382 | # VS Code files for those working on multiple tools 383 | .vscode/* 384 | !.vscode/settings.json 385 | !.vscode/tasks.json 386 | !.vscode/launch.json 387 | !.vscode/extensions.json 388 | *.code-workspace 389 | 390 | # Local History for Visual Studio Code 391 | .history/ 392 | 393 | # Windows Installer files from build outputs 394 | *.cab 395 | *.msi 396 | *.msix 397 | *.msm 398 | *.msp 399 | 400 | # JetBrains Rider 401 | *.sln.iml 402 | 403 | ## 404 | ## Visual studio for Mac 405 | ## 406 | 407 | 408 | # globs 409 | Makefile.in 410 | *.userprefs 411 | *.usertasks 412 | config.make 413 | config.status 414 | aclocal.m4 415 | install-sh 416 | autom4te.cache/ 417 | *.tar.gz 418 | tarballs/ 419 | test-results/ 420 | 421 | # Mac bundle stuff 422 | *.dmg 423 | *.app 424 | 425 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 426 | # General 427 | .DS_Store 428 | .AppleDouble 429 | .LSOverride 430 | 431 | # Icon must end with two \r 432 | Icon 433 | 434 | 435 | # Thumbnails 436 | ._* 437 | 438 | # Files that might appear in the root of a volume 439 | .DocumentRevisions-V100 440 | .fseventsd 441 | .Spotlight-V100 442 | .TemporaryItems 443 | .Trashes 444 | .VolumeIcon.icns 445 | .com.apple.timemachine.donotpresent 446 | 447 | # Directories potentially created on remote AFP share 448 | .AppleDB 449 | .AppleDesktop 450 | Network Trash Folder 451 | Temporary Items 452 | .apdisk 453 | 454 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 455 | # Windows thumbnail cache files 456 | Thumbs.db 457 | ehthumbs.db 458 | ehthumbs_vista.db 459 | 460 | # Dump file 461 | *.stackdump 462 | 463 | # Folder config file 464 | [Dd]esktop.ini 465 | 466 | # Recycle Bin used on file shares 467 | $RECYCLE.BIN/ 468 | 469 | # Windows Installer files 470 | *.cab 471 | *.msi 472 | *.msix 473 | *.msm 474 | *.msp 475 | 476 | # Windows shortcuts 477 | *.lnk 478 | 479 | .idea/ 480 | --------------------------------------------------------------------------------