├── 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 |
--------------------------------------------------------------------------------