├── .gitattributes
├── .gitignore
├── AvaloniaDemos.sln
├── BlockPatternAnimation
├── App.axaml
├── App.axaml.cs
├── Assets
│ ├── avalonia-logo.ico
│ └── demoScreenCapture.gif
├── BlockPatternAnimation.csproj
├── Program.cs
├── README.md
└── Views
│ ├── MainWindow.axaml
│ └── MainWindow.axaml.cs
├── Debugging
├── App.axaml
├── App.axaml.cs
├── Assets
│ ├── FontAwesome
│ │ ├── Font Awesome 6 Brands-Regular-400.otf
│ │ ├── Font Awesome 6 Free-Regular-400.otf
│ │ └── Font Awesome 6 Free-Solid-900.otf
│ ├── JetBrainsMono
│ │ ├── JetBrainsMono-Bold-Italic.ttf
│ │ ├── JetBrainsMono-Bold.ttf
│ │ ├── JetBrainsMono-ExtraBold-Italic.ttf
│ │ ├── JetBrainsMono-ExtraBold.ttf
│ │ ├── JetBrainsMono-Italic.ttf
│ │ ├── JetBrainsMono-Medium-Italic.ttf
│ │ ├── JetBrainsMono-Medium.ttf
│ │ └── JetBrainsMono-Regular.ttf
│ ├── assetScreenCapture.gif
│ ├── avalonia-logo.ico
│ └── logFindScreenCapture.gif
├── DebugPlus
│ ├── DebugPlus.cs
│ ├── DebugPlusLogExtensions.cs
│ ├── DebugPlusLogSink.cs
│ ├── Native
│ │ └── HBNative.cs
│ └── StringBuilderCache.cs
├── Debugging.csproj
├── Program.cs
├── README.md
├── ViewLocator.cs
├── ViewModels
│ ├── MainWindowViewModel.cs
│ └── ViewModelBase.cs
├── Views
│ ├── MainWindow.axaml
│ └── MainWindow.axaml.cs
└── app.manifest
├── Dialogs.Abstractions
├── DialogViewModel.cs
├── Dialogs.Abstractions.csproj
├── IInteractionService.cs
├── IRequestMediator.cs
├── Prompts.cs
└── README.md
├── Dialogs.Todo
├── App.axaml
├── App.axaml.cs
├── Assets
│ ├── avalonia-logo.ico
│ ├── todoadditem.gif
│ └── todoeditsaveload.gif
├── Converters
│ ├── AppConverters.cs
│ └── EnumToBooleanConverter.cs
├── Dialogs.Todo.csproj
├── Mappers
│ └── TodoMappers.cs
├── Models
│ └── TodoModel.cs
├── PresentationFactory.cs
├── Program.cs
├── README.md
├── Resources
│ └── AppIcons.cs
├── Services
│ ├── FileRequestService.cs
│ ├── InteractionService.cs
│ └── TodoService.cs
├── Styles
│ └── AppStyles.axaml
├── ViewLocator.cs
├── ViewModels
│ ├── MainWindowViewModel.cs
│ ├── TodoCollectionViewModel.cs
│ ├── TodoEditorViewModel.cs
│ └── TodoViewModel.cs
├── Views
│ ├── MainWindow.axaml
│ ├── MainWindow.axaml.cs
│ ├── TodoEditorView.axaml
│ └── TodoEditorView.axaml.cs
├── _data
│ └── default.json
└── app.manifest
├── Directory.Build.props
├── Directory.Packages.props
├── ImageHotspot
├── App.axaml
├── App.axaml.cs
├── Assets
│ ├── controller.jpg
│ ├── controller.svg
│ └── demoScreenCapture.gif
├── ImageHotspot.csproj
├── MainWindow.axaml
├── MainWindow.axaml.cs
├── Program.cs
├── README.md
└── app.manifest
├── LICENSE.md
├── NoSchemaCsv.Immutable
├── App.axaml
├── App.axaml.cs
├── Assets
│ ├── avalonia-logo.ico
│ └── noschemacsv-immutable.gif
├── Messages.cs
├── NoSchemaCsv.Immutable.csproj
├── Program.cs
├── README.md
├── Services
│ ├── CsvGeneratorService.cs
│ └── CsvService.cs
├── ViewLocator.cs
├── ViewModels
│ ├── MainWindowViewModel.cs
│ └── ViewModelBase.cs
├── Views
│ ├── MainWindow.axaml
│ └── MainWindow.axaml.cs
└── app.manifest
├── NuGet.config
├── README.md
├── RealTimeBitmapAdapter
├── App.axaml
├── App.axaml.cs
├── Assets
│ ├── avalonia-logo.ico
│ └── demoScreenCapture.png
├── ImageAdapters
│ ├── BitmapAdapter.cs
│ └── WriteableBitmapAdapter.cs
├── Models
│ ├── ColorScaleImage.cs
│ └── ImageBase.cs
├── Program.cs
├── README.md
├── RealTimeBitmapAdapter.csproj
├── Rng
│ └── XoshiroRandom.cs
├── ViewLocator.cs
├── ViewModels
│ ├── MainWindowViewModel.cs
│ └── ViewModelBase.cs
└── Views
│ ├── MainWindow.axaml
│ └── MainWindow.axaml.cs
├── Settings.XamlStyler
├── SkiaBitmapAdapter
├── App.axaml
├── App.axaml.cs
├── Assets
│ ├── avalonia-logo.ico
│ └── skbitmapscroll.gif
├── Program.cs
├── README.md
├── SkiaBitmapAdapter.csproj
├── ViewModels
│ ├── MainWindowViewModel.cs
│ └── SkiaBitmapViewModel.cs
├── Views
│ ├── MainWindow.axaml
│ ├── MainWindow.axaml.cs
│ ├── SkiaBitmapView.axaml
│ └── SkiaBitmapView.axaml.cs
└── app.manifest
├── SkiaRendering.ExpandingCircles
├── App.axaml
├── App.axaml.cs
├── AppConverters.cs
├── Assets
│ ├── avalonia-logo.ico
│ └── expandingcircles.gif
├── Messages.cs
├── Program.cs
├── README.md
├── SkiaRendering.ExpandingCircles.csproj
├── ViewModels
│ ├── CircleViewModel.cs
│ ├── MainWindowViewModel.cs
│ ├── ObservableElement.cs
│ ├── ViewModelBase.cs
│ └── WorldViewModel.cs
├── Views
│ ├── MainWindow.axaml
│ └── MainWindow.axaml.cs
└── app.manifest
├── SkiaRendering.InfiniteCanvas
├── InfiniteCanvas.cs
├── InfiniteCanvas.props.cs
├── README.md
├── SKPaintSurfaceEventArgs.cs
├── SkiaRendering.InfiniteCanvas.csproj
└── UpdateStateEventArgs.cs
├── SwipeNavigation
├── App.axaml
├── App.axaml.cs
├── Assets
│ └── swipenavigation.gif
├── Gestures
│ ├── CustomGestures.cs
│ ├── SwipeGestureEndedEventArgs.cs
│ ├── SwipeGestureEventArgs.cs
│ ├── SwipeGestureRecognizer.cs
│ └── SwipeGestureRecognizer.props.cs
├── MainView.axaml
├── MainView.axaml.cs
├── MainWindow.axaml
├── MainWindow.axaml.cs
├── Pages
│ ├── PageOne.axaml
│ ├── PageOne.axaml.cs
│ ├── PageThree.axaml
│ ├── PageThree.axaml.cs
│ ├── PageTwo.axaml
│ └── PageTwo.axaml.cs
├── Program.cs
├── README.md
├── SwipeNavigation.csproj
├── TransitioningPageControl
│ ├── TransitioningPageControl.cs
│ └── TransitioningPageControl.props.cs
└── app.manifest
├── TabStripViewCaching
├── App.axaml
├── App.axaml.cs
├── Assets
│ ├── avalonia-logo.ico
│ └── tabstripcaching.gif
├── Program.cs
├── README.md
├── TabStripViewCaching.csproj
├── ViewLocator.cs
├── ViewModels
│ ├── MainWindowViewModel.cs
│ ├── PersonViewModel.cs
│ └── TabViewModel.cs
├── Views
│ ├── MainWindow.axaml
│ ├── MainWindow.axaml.cs
│ ├── PersonView.axaml
│ └── PersonView.axaml.cs
└── app.manifest
├── TextHighlighting
├── App.axaml
├── App.axaml.cs
├── Assets
│ ├── textblockhighlighting.gif
│ └── textboxhighlighting.gif
├── Highlighting
│ ├── HighlightRangeCache.cs
│ └── IHighlighter.cs
├── MainWindow.axaml
├── MainWindow.axaml.cs
├── Program.cs
├── README.md
├── TextHighlightBlock
│ ├── TextHighlightBlock.cs
│ └── TextHighlightBlock.props.cs
├── TextHighlightBox
│ ├── HighlightingTextPresenter.cs
│ ├── HighlightingTextPresenter.props.cs
│ ├── StringBuilderCache.cs
│ ├── TextHighlightBox.cs
│ ├── TextHighlightBox.props.cs
│ └── TextHighlightBoxTheme.axaml
├── TextHighlighting.csproj
└── app.manifest
├── TextOutlineAnimation
├── App.axaml
├── App.axaml.cs
├── Assets
│ ├── avalonia-logo.ico
│ └── demoScreenCapture.gif
├── Controls
│ ├── OutlinedText.cs
│ └── OutlinedText.props.cs
├── Program.cs
├── README.md
├── TextOutlineAnimation.csproj
├── ViewLocator.cs
├── ViewModels
│ ├── MainWindowViewModel.cs
│ └── ViewModelBase.cs
└── Views
│ ├── MainWindow.axaml
│ └── MainWindow.axaml.cs
└── TransparentBrushTransitions
├── App.axaml
├── App.axaml.cs
├── Assets
└── transparentbrushtransitions.gif
├── LerpHelpers.cs
├── MainWindow.axaml
├── MainWindow.axaml.cs
├── Program.cs
├── README.md
├── TransparentBrushTransition.cs
├── TransparentBrushTransitions.csproj
└── app.manifest
/.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 |
--------------------------------------------------------------------------------
/BlockPatternAnimation/App.axaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/BlockPatternAnimation/App.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls.ApplicationLifetimes;
3 | using Avalonia.Markup.Xaml;
4 | using BlockPatternAnimation.Views;
5 |
6 | namespace BlockPatternAnimation;
7 | public partial class App : Application
8 | {
9 | public override void Initialize()
10 | {
11 | AvaloniaXamlLoader.Load(this);
12 | }
13 |
14 | public override void OnFrameworkInitializationCompleted()
15 | {
16 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
17 | {
18 | desktop.MainWindow = new MainWindow();
19 | }
20 |
21 | base.OnFrameworkInitializationCompleted();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/BlockPatternAnimation/Assets/avalonia-logo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevemonaco/AvaloniaDemos/2a64e0037da9902cb65729ec83ecfe3c2bef2fb9/BlockPatternAnimation/Assets/avalonia-logo.ico
--------------------------------------------------------------------------------
/BlockPatternAnimation/Assets/demoScreenCapture.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevemonaco/AvaloniaDemos/2a64e0037da9902cb65729ec83ecfe3c2bef2fb9/BlockPatternAnimation/Assets/demoScreenCapture.gif
--------------------------------------------------------------------------------
/BlockPatternAnimation/BlockPatternAnimation.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | WinExe
4 | true
5 | true
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/BlockPatternAnimation/Program.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using System;
3 |
4 | namespace BlockPatternAnimation;
5 | internal class Program
6 | {
7 | // Initialization code. Don't use any Avalonia, third-party APIs or any
8 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
9 | // yet and stuff might break.
10 | [STAThread]
11 | public static void Main(string[] args) => BuildAvaloniaApp()
12 | .StartWithClassicDesktopLifetime(args);
13 |
14 | // Avalonia configuration, don't remove; also used by visual designer.
15 | public static AppBuilder BuildAvaloniaApp()
16 | => AppBuilder.Configure()
17 | .UsePlatformDetect()
18 | .LogToTrace();
19 | }
20 |
--------------------------------------------------------------------------------
/BlockPatternAnimation/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | Effect is implemented with multiple stacked `Grid`s, `VisualBrush`, and `TranslateTransform`:
4 |
5 | - Static layer with background solid color
6 | - Static layer with large squares
7 | - Animated layer of square-in-squares that move left and up
8 | - Animated layer of square-in-squares that move right and down
9 |
10 | [Original implementation](https://codepen.io/t_afif/full/OJvBbxm) in CSS by Temani Afif
11 |
12 | Thanks to [Starlk](https://github.com/starlkyt) for `Margin` suggestion to remove a grid resizing hack
13 |
--------------------------------------------------------------------------------
/BlockPatternAnimation/Views/MainWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Controls;
2 |
3 | namespace BlockPatternAnimation.Views;
4 | public partial class MainWindow : Window
5 | {
6 | public MainWindow()
7 | {
8 | InitializeComponent();
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Debugging/App.axaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | avares://Debugging/Assets/JetBrainsMono#JetBrains Mono
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Debugging/App.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls.ApplicationLifetimes;
3 | using Avalonia.Data.Core.Plugins;
4 | using Avalonia.Markup.Xaml;
5 | using Debugging.ViewModels;
6 | using Debugging.Views;
7 |
8 | namespace Debugging;
9 | public partial class App : Application
10 | {
11 | public override void Initialize()
12 | {
13 | AvaloniaXamlLoader.Load(this);
14 | }
15 |
16 | public override void OnFrameworkInitializationCompleted()
17 | {
18 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
19 | {
20 | // Line below is needed to remove Avalonia data validation.
21 | // Without this line you will get duplicate validations from both Avalonia and CT
22 | BindingPlugins.DataValidators.RemoveAt(0);
23 | desktop.MainWindow = new MainWindow
24 | {
25 | DataContext = new MainWindowViewModel(),
26 | };
27 | }
28 |
29 | base.OnFrameworkInitializationCompleted();
30 | }
31 | }
--------------------------------------------------------------------------------
/Debugging/Assets/FontAwesome/Font Awesome 6 Brands-Regular-400.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevemonaco/AvaloniaDemos/2a64e0037da9902cb65729ec83ecfe3c2bef2fb9/Debugging/Assets/FontAwesome/Font Awesome 6 Brands-Regular-400.otf
--------------------------------------------------------------------------------
/Debugging/Assets/FontAwesome/Font Awesome 6 Free-Regular-400.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevemonaco/AvaloniaDemos/2a64e0037da9902cb65729ec83ecfe3c2bef2fb9/Debugging/Assets/FontAwesome/Font Awesome 6 Free-Regular-400.otf
--------------------------------------------------------------------------------
/Debugging/Assets/FontAwesome/Font Awesome 6 Free-Solid-900.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevemonaco/AvaloniaDemos/2a64e0037da9902cb65729ec83ecfe3c2bef2fb9/Debugging/Assets/FontAwesome/Font Awesome 6 Free-Solid-900.otf
--------------------------------------------------------------------------------
/Debugging/Assets/JetBrainsMono/JetBrainsMono-Bold-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevemonaco/AvaloniaDemos/2a64e0037da9902cb65729ec83ecfe3c2bef2fb9/Debugging/Assets/JetBrainsMono/JetBrainsMono-Bold-Italic.ttf
--------------------------------------------------------------------------------
/Debugging/Assets/JetBrainsMono/JetBrainsMono-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevemonaco/AvaloniaDemos/2a64e0037da9902cb65729ec83ecfe3c2bef2fb9/Debugging/Assets/JetBrainsMono/JetBrainsMono-Bold.ttf
--------------------------------------------------------------------------------
/Debugging/Assets/JetBrainsMono/JetBrainsMono-ExtraBold-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevemonaco/AvaloniaDemos/2a64e0037da9902cb65729ec83ecfe3c2bef2fb9/Debugging/Assets/JetBrainsMono/JetBrainsMono-ExtraBold-Italic.ttf
--------------------------------------------------------------------------------
/Debugging/Assets/JetBrainsMono/JetBrainsMono-ExtraBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevemonaco/AvaloniaDemos/2a64e0037da9902cb65729ec83ecfe3c2bef2fb9/Debugging/Assets/JetBrainsMono/JetBrainsMono-ExtraBold.ttf
--------------------------------------------------------------------------------
/Debugging/Assets/JetBrainsMono/JetBrainsMono-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevemonaco/AvaloniaDemos/2a64e0037da9902cb65729ec83ecfe3c2bef2fb9/Debugging/Assets/JetBrainsMono/JetBrainsMono-Italic.ttf
--------------------------------------------------------------------------------
/Debugging/Assets/JetBrainsMono/JetBrainsMono-Medium-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevemonaco/AvaloniaDemos/2a64e0037da9902cb65729ec83ecfe3c2bef2fb9/Debugging/Assets/JetBrainsMono/JetBrainsMono-Medium-Italic.ttf
--------------------------------------------------------------------------------
/Debugging/Assets/JetBrainsMono/JetBrainsMono-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevemonaco/AvaloniaDemos/2a64e0037da9902cb65729ec83ecfe3c2bef2fb9/Debugging/Assets/JetBrainsMono/JetBrainsMono-Medium.ttf
--------------------------------------------------------------------------------
/Debugging/Assets/JetBrainsMono/JetBrainsMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevemonaco/AvaloniaDemos/2a64e0037da9902cb65729ec83ecfe3c2bef2fb9/Debugging/Assets/JetBrainsMono/JetBrainsMono-Regular.ttf
--------------------------------------------------------------------------------
/Debugging/Assets/assetScreenCapture.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevemonaco/AvaloniaDemos/2a64e0037da9902cb65729ec83ecfe3c2bef2fb9/Debugging/Assets/assetScreenCapture.gif
--------------------------------------------------------------------------------
/Debugging/Assets/avalonia-logo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevemonaco/AvaloniaDemos/2a64e0037da9902cb65729ec83ecfe3c2bef2fb9/Debugging/Assets/avalonia-logo.ico
--------------------------------------------------------------------------------
/Debugging/Assets/logFindScreenCapture.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevemonaco/AvaloniaDemos/2a64e0037da9902cb65729ec83ecfe3c2bef2fb9/Debugging/Assets/logFindScreenCapture.gif
--------------------------------------------------------------------------------
/Debugging/DebugPlus/DebugPlusLogExtensions.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Logging;
3 | using Microsoft.Extensions.Logging;
4 |
5 | namespace Monaco.Debugging;
6 | public static class DebugPlusLogExtensions
7 | {
8 | public static AppBuilder LogToDebugPlus(this AppBuilder builder,
9 | LogEventLevel level = LogEventLevel.Warning,
10 | ILogger? logger = null,
11 | params string[] areas)
12 | {
13 | Logger.Sink = new DebugPlusLogSink(level, areas, logger);
14 | return builder;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Debugging/DebugPlus/Native/HBNative.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 | using HarfBuzzSharp;
3 |
4 | namespace Monaco.Debugging.Native;
5 |
6 | // Taken from HarfBuzzSharp's internal generated bindings
7 | public unsafe static class HBNative
8 | {
9 | #if __IOS__ || __TVOS__
10 | private const string _harfBuzz = "@rpath/libHarfBuzzSharp.framework/libHarfBuzzSharp";
11 | #else
12 | private const string _harfBuzz = "libHarfBuzzSharp";
13 | #endif
14 |
15 | [DllImport(_harfBuzz, CallingConvention = CallingConvention.Cdecl)]
16 | internal static extern uint hb_ot_name_get_utf8(nint face, OpenTypeNameId name_id, nint language, uint* text_size, /* char */ void* text);
17 |
18 | [DllImport(_harfBuzz, CallingConvention = CallingConvention.Cdecl)]
19 | internal static extern nint hb_language_from_string([MarshalAs(UnmanagedType.LPStr)] string str, int len);
20 | }
21 |
--------------------------------------------------------------------------------
/Debugging/DebugPlus/StringBuilderCache.cs:
--------------------------------------------------------------------------------
1 | // Licensed to the .NET Foundation under one or more agreements.
2 | // The .NET Foundation licenses this file to you under the MIT license.
3 | using System;
4 | using System.Text;
5 |
6 | namespace Monaco.Debugging;
7 |
8 | /// Provide a cached reusable instance of stringbuilder per thread.
9 | internal static class StringBuilderCache
10 | {
11 | // The value 360 was chosen in discussion with performance experts as a compromise between using
12 | // as little memory per thread as possible and still covering a large part of short-lived
13 | // StringBuilder creations on the startup path of VS designers.
14 | internal const int MaxBuilderSize = 360;
15 | private const int DefaultCapacity = 16; // == StringBuilder.DefaultCapacity
16 |
17 | [ThreadStatic]
18 | private static StringBuilder? t_cachedInstance;
19 |
20 | /// Get a StringBuilder for the specified capacity.
21 | /// If a StringBuilder of an appropriate size is cached, it will be returned and the cache emptied.
22 | public static StringBuilder Acquire(int capacity = DefaultCapacity)
23 | {
24 | if (capacity <= MaxBuilderSize)
25 | {
26 | StringBuilder? sb = t_cachedInstance;
27 | if (sb != null)
28 | {
29 | // Avoid stringbuilder block fragmentation by getting a new StringBuilder
30 | // when the requested size is larger than the current capacity
31 | if (capacity <= sb.Capacity)
32 | {
33 | t_cachedInstance = null;
34 | sb.Clear();
35 | return sb;
36 | }
37 | }
38 | }
39 |
40 | return new StringBuilder(capacity);
41 | }
42 |
43 | /// Place the specified builder in the cache if it is not too big.
44 | public static void Release(StringBuilder sb)
45 | {
46 | if (sb.Capacity <= MaxBuilderSize)
47 | {
48 | t_cachedInstance = sb;
49 | }
50 | }
51 |
52 | /// ToString() the stringbuilder, Release it to the cache, and return the resulting string.
53 | public static string GetStringAndRelease(StringBuilder sb)
54 | {
55 | string result = sb.ToString();
56 | Release(sb);
57 | return result;
58 | }
59 | }
--------------------------------------------------------------------------------
/Debugging/Debugging.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | WinExe
4 | true
5 | app.manifest
6 | true
7 | true
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/Debugging/Program.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Monaco.Debugging;
3 | using System;
4 |
5 | namespace Debugging;
6 |
7 | internal sealed class Program
8 | {
9 | [STAThread]
10 | public static void Main(string[] args) => BuildAvaloniaApp()
11 | .StartWithClassicDesktopLifetime(args);
12 |
13 | public static AppBuilder BuildAvaloniaApp()
14 | => AppBuilder.Configure()
15 | .UsePlatformDetect()
16 | .WithInterFont()
17 | .LogToDebugPlus(); // Register custom ILogSink
18 | }
19 |
--------------------------------------------------------------------------------
/Debugging/README.md:
--------------------------------------------------------------------------------
1 | Collection of small debugging improvements over what's available in DevTools. It is necessary to add these into your own app, there's no attachment like DevTools. Most functionality is in
2 | the DebugPlus folder.
3 |
4 | ## App Binary Diagnostics
5 |
6 | * Assembly Listing
7 | * Asset Listing
8 | * Font Name IDs Listing
9 |
10 | Knowing what binaries your app relies upon is important. This demonstrates how to list all assemblies and assets for the current app while it is running.
11 |
12 | Fonts have long been a confusing part of XAML as the rules are a bit different. The filename has no effect on the Uri that needs to be referenced. The key name is created from
13 | the Typographic Family Name (or fallback to Font Family Name if none available) which are two fields within the Name IDs section. This feature predicts what the full Uri
14 | should be by reading the Name IDs from a font included as an asset and provides the XAML necessary to create a `FontFamily`.
15 |
16 | [FontDrop](https://fontdrop.info/#/?darkmode=true) and [msdocs](https://learn.microsoft.com/en-us/typography/opentype/spec/name#name-ids) were helpful in the font area.
17 |
18 | 
19 |
20 | ## Improved Locating for Bad Bindings
21 |
22 | * Customized ILogSink with Visual Tree Information
23 | * Lookup Control via Hash ID
24 |
25 | Locating the source of bad bindings has been a long-running problem, mitigated by use of compiled bindings more recently. However, it's still occasionally an issue. Avalonia logs
26 | these binding errors and prints out the Hash ID when the Control containing the bad binding doesn't have a name. `DebugPlusLogSink` changes this so that all events will
27 | log the Hash ID, Name (if available), and the Visual Tree hierarchy.
28 |
29 | When you have the Control Hash ID, you can locate the actual Control by searching the Visual Tree. The capture below demonstrates this.
30 |
31 | 
32 |
33 | The logged message is:
34 | ```
35 | [Binding]An error occurred binding 'Command' to 'BadBinding' at 'BadBinding': 'Could not find a matching property accessor for 'BadBinding' on 'Debugging.ViewModels.MainWindowViewModel'.' (Button #36936550 #badButton Visual Tree: Grid -> StackPanel -> Button)
36 | ```
37 |
--------------------------------------------------------------------------------
/Debugging/ViewLocator.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Controls;
2 | using Avalonia.Controls.Templates;
3 | using Debugging.ViewModels;
4 | using System;
5 |
6 | namespace Debugging;
7 | public class ViewLocator : IDataTemplate
8 | {
9 | public Control? Build(object? data)
10 | {
11 | if (data is null)
12 | return null;
13 |
14 | var name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
15 | var type = Type.GetType(name);
16 |
17 | if (type != null)
18 | {
19 | var control = (Control)Activator.CreateInstance(type)!;
20 | control.DataContext = data;
21 | return control;
22 | }
23 |
24 | return new TextBlock { Text = "Not Found: " + name };
25 | }
26 |
27 | public bool Match(object? data)
28 | {
29 | return data is ViewModelBase;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Debugging/ViewModels/MainWindowViewModel.cs:
--------------------------------------------------------------------------------
1 | using CommunityToolkit.Mvvm.ComponentModel;
2 | using CommunityToolkit.Mvvm.Input;
3 | using Monaco.Debugging;
4 | using System.Collections.ObjectModel;
5 | using System;
6 | using System.Linq;
7 | using System.IO;
8 |
9 | namespace Debugging.ViewModels;
10 |
11 | public partial class MainWindowViewModel : ViewModelBase
12 | {
13 | [ObservableProperty] private ObservableCollection _display = new();
14 | [ObservableProperty] private bool _filterResourceXamlInfo = false;
15 | [ObservableProperty] private string _fontUri = "";
16 |
17 | [RelayCommand]
18 | public void ListAssemblies()
19 | {
20 | Display.Clear();
21 |
22 | foreach (var assembly in DebugPlus.GetAllAssemblyInfo())
23 | {
24 | Display.Add($"Assembly Name: {assembly.Name}, URI: {assembly.Uri?.AbsoluteUri ?? "Unavailable"}");
25 | }
26 | }
27 |
28 | [RelayCommand]
29 | public void ListAssets()
30 | {
31 | Display.Clear();
32 |
33 | var infos = DebugPlus.GetAllAssetInfo()
34 | .Where(x => FilterResourceXamlInfo ? !x.Uri.AbsolutePath.Contains("!AvaloniaResourceXamlInfo") : true);
35 |
36 | foreach (var info in infos)
37 | {
38 | Display.Add($"Asset URI: {info.Uri} | Size: {info.Length}");
39 | }
40 | }
41 |
42 | [RelayCommand]
43 | public void ListFontIdNamesCommand()
44 | {
45 | if (string.IsNullOrWhiteSpace(FontUri))
46 | return;
47 |
48 | Display.Clear();
49 |
50 | var uri = new Uri(FontUri);
51 | var infos = DebugPlus.GetNameIdsFromFontUri(uri);
52 | var fontName = infos.FirstOrDefault(x => x.Id == "FontFamily")?.Name;
53 | var typographicName = infos.FirstOrDefault(x => x.Id == "TypographicFamily")?.Name;
54 |
55 | // Poor code for building an avares:// scheme without the font file name
56 | if (Path.GetDirectoryName(FontUri) is not string initialFolder)
57 | return;
58 |
59 | var fontFolder = initialFolder.Replace('\\', '/').Replace("avares:/", "avares://");
60 |
61 | string? standardFontName = string.IsNullOrEmpty(typographicName) ? fontName : typographicName;
62 | string resourceFontName = $"{fontFolder}#{standardFontName}";
63 |
64 | foreach (var info in infos)
65 | {
66 | Display.Add($"{info.Id}: {info.Name}");
67 | }
68 |
69 | Display.Add($@"!Avalonia Resource: {resourceFontName}");
70 | Display.Add($@"!AXAML Snippet: {resourceFontName}");
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Debugging/ViewModels/ViewModelBase.cs:
--------------------------------------------------------------------------------
1 | using CommunityToolkit.Mvvm.ComponentModel;
2 |
3 | namespace Debugging.ViewModels;
4 | public class ViewModelBase : ObservableObject
5 | {
6 | }
7 |
--------------------------------------------------------------------------------
/Debugging/Views/MainWindow.axaml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/Debugging/Views/MainWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using Avalonia.Controls;
3 | using Avalonia.Interactivity;
4 | using Avalonia.VisualTree;
5 | using Avalonia.Input;
6 | using Avalonia;
7 | using Avalonia.Controls.Primitives;
8 |
9 | namespace Debugging.Views;
10 | public partial class MainWindow : Window
11 | {
12 | public MainWindow()
13 | {
14 | InitializeComponent();
15 | AddHandler(PointerPressedEvent, Window_PointerPressed, RoutingStrategies.Tunnel);
16 | }
17 |
18 | private void Search_Click(object? sender, RoutedEventArgs e)
19 | {
20 | if (string.IsNullOrWhiteSpace(controlHashInput.Text))
21 | return;
22 |
23 | if (!int.TryParse(controlHashInput.Text, out var searchHash))
24 | {
25 | searchResult.Text = $"Could not parse '{controlHashInput.Text}' as integer";
26 | return;
27 | }
28 |
29 | var visual = this.GetSelfAndVisualDescendants().FirstOrDefault(x => x.GetHashCode() == searchHash);
30 |
31 | if (visual is null)
32 | {
33 | searchResult.Text = $"No control found for #{searchHash}";
34 | }
35 | else
36 | {
37 | var tree = FormatVisualTree(visual);
38 | searchResult.Text = $"Type: {visual.GetType()}\nName: {visual.Name}\nVisual Tree: {tree}";
39 | }
40 | }
41 |
42 | private void Window_PointerPressed(object? sender, PointerPressedEventArgs e)
43 | {
44 | var point = e.GetCurrentPoint(this);
45 | searchResult.Text = "";
46 |
47 | if (point.Properties.IsLeftButtonPressed && e.KeyModifiers == KeyModifiers.Control)
48 | {
49 | var tl = GetTopLevel(this);
50 | var inputElement = GetControlAtPoint(tl!, point.Position);
51 |
52 | if (inputElement is not Control control)
53 | {
54 | controlHashInput.Text = "";
55 | return;
56 | }
57 |
58 | var controlVisitor = control;
59 |
60 | while (controlVisitor.TemplatedParent is not null)
61 | controlVisitor = (Control)controlVisitor.TemplatedParent;
62 |
63 | controlHashInput.Text = controlVisitor.GetHashCode().ToString();
64 |
65 | e.Handled = true;
66 | }
67 | }
68 |
69 | private Control? GetControlAtPoint(TopLevel topLevel, Point p)
70 | {
71 | // Code adapted from DevTools (Avalonia.Diagnostics)
72 | return (Control?)topLevel.GetVisualsAt(p, x =>
73 | {
74 | if (x is AdornerLayer || !x.IsVisible)
75 | {
76 | return false;
77 | }
78 |
79 | return !(x is IInputElement ie) || ie.IsHitTestVisible;
80 | })
81 | .FirstOrDefault();
82 | }
83 |
84 | private static string FormatVisualTree(Visual visual)
85 | {
86 | return string.Join(" -> ", visual.GetSelfAndVisualAncestors().Reverse().Select(x => x.GetType().Name));
87 | }
88 | }
--------------------------------------------------------------------------------
/Debugging/app.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/Dialogs.Abstractions/DialogViewModel.cs:
--------------------------------------------------------------------------------
1 | using CommunityToolkit.Mvvm.ComponentModel;
2 | using CommunityToolkit.Mvvm.Input;
3 |
4 | namespace Dialogs.Abstractions;
5 | public abstract partial class DialogViewModel : ObservableValidator, IRequestMediator
6 | {
7 | [ObservableProperty] protected TResult? _requestResult = default;
8 | [ObservableProperty] private string _title = "";
9 |
10 | private RelayCommand? _acceptCommand;
11 | public IRelayCommand AcceptCommand => _acceptCommand ??= new RelayCommand(Accept, CanAccept);
12 |
13 | private RelayCommand? _cancelCommand;
14 | public IRelayCommand CancelCommand => _cancelCommand ??= new RelayCommand(Cancel);
15 |
16 | public string AcceptName { get; init; } = "Ok";
17 | public string CancelName { get; init; } = "Cancel";
18 |
19 | ///
20 | /// Called when the user accepts the interaction
21 | /// Responsible for mapping an internal result into RequestResult
22 | ///
23 | protected abstract void Accept();
24 |
25 | protected virtual bool CanAccept() => true;
26 |
27 | ///
28 | /// Called when the user cancels an interaction
29 | ///
30 | [System.Diagnostics.CodeAnalysis.SuppressMessage("ObservablePropertyGenerator", "MVVMTK0034",
31 | Justification = "SetProperty must be used here to ensure PropertyChanged always fires")]
32 | protected virtual void Cancel()
33 | {
34 | SetProperty(ref _requestResult, default, false);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Dialogs.Abstractions/Dialogs.Abstractions.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | enable
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Dialogs.Abstractions/IInteractionService.cs:
--------------------------------------------------------------------------------
1 | namespace Dialogs.Abstractions;
2 |
3 | ///
4 | /// Abstraction representing an user interaction which requires a response
5 | ///
6 | public interface IInteractionService
7 | {
8 | ///
9 | /// Displays a message to the user
10 | ///
11 | /// Heading text
12 | /// Main body of text
13 | Task AlertAsync(string heading, string message);
14 |
15 | ///
16 | /// Prompts the user to make a choice
17 | ///
18 | /// Choices to present to the user
19 | /// Heading text
20 | /// Main body of text
21 | /// The result of the user choice
22 | Task PromptAsync(PromptChoice choices, string heading, string? message = default);
23 |
24 | ///
25 | /// Requests an interaction with the user
26 | ///
27 | /// Result of the interaction
28 | /// The mediation object to interact with
29 | /// The result of the interaction
30 | Task RequestAsync(IRequestMediator mediator);
31 | }
32 |
--------------------------------------------------------------------------------
/Dialogs.Abstractions/IRequestMediator.cs:
--------------------------------------------------------------------------------
1 | using CommunityToolkit.Mvvm.Input;
2 | using System.ComponentModel;
3 |
4 | namespace Dialogs.Abstractions;
5 |
6 | ///
7 | /// Abstraction representing an extended interaction with the user, typically a dialog
8 | ///
9 | ///
10 | public interface IRequestMediator : INotifyPropertyChanged
11 | {
12 | string Title { get; }
13 | string AcceptName { get; }
14 | string CancelName { get; }
15 | TResult? RequestResult { get; set; }
16 |
17 | IRelayCommand AcceptCommand { get; }
18 | IRelayCommand CancelCommand { get; }
19 | }
20 |
--------------------------------------------------------------------------------
/Dialogs.Abstractions/Prompts.cs:
--------------------------------------------------------------------------------
1 | namespace Dialogs.Abstractions;
2 |
3 | public enum PromptResult { Accept, Reject, Cancel }
4 | public record PromptChoice(string? Accept = null, string? Reject = null, string? Cancel = null);
5 |
6 | public static class PromptChoices
7 | {
8 | public static PromptChoice Ok { get; } = new("Ok");
9 | public static PromptChoice OkCancel { get; } = new("Ok", null, "Cancel");
10 | public static PromptChoice YesNo { get; } = new("Yes", "No");
11 | public static PromptChoice YesNoCancel { get; } = new("Yes", "No", "Cancel");
12 | }
13 |
--------------------------------------------------------------------------------
/Dialogs.Abstractions/README.md:
--------------------------------------------------------------------------------
1 | This project contains abstractions for implementing a MVVM-friendly dialog system. The types are not coupled to any View, so the View will need to implement them for the particular UI framework. `DialogViewModel` is coupled to Mvvm Toolkit.
2 |
3 | See [Dialogs.Todo](/Dialogs.Todo) for an example project.
4 |
--------------------------------------------------------------------------------
/Dialogs.Todo/App.axaml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/Dialogs.Todo/App.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls.ApplicationLifetimes;
3 | using Avalonia.Data.Core.Plugins;
4 | using Avalonia.Markup.Xaml;
5 | using CommunityToolkit.Mvvm.DependencyInjection;
6 | using Dialogs.Abstractions;
7 | using Dialogs.Services;
8 | using Dialogs.ViewModels;
9 | using Dialogs.Views;
10 | using Microsoft.Extensions.DependencyInjection;
11 |
12 | namespace Dialogs;
13 | public partial class App : Application
14 | {
15 | public override void Initialize()
16 | {
17 | AvaloniaXamlLoader.Load(this);
18 | }
19 |
20 | public override void OnFrameworkInitializationCompleted()
21 | {
22 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
23 | {
24 | BindingPlugins.DataValidators.RemoveAt(0);
25 |
26 | var services = new ServiceCollection();
27 | services.AddSingleton();
28 | services.AddSingleton();
29 | services.AddSingleton();
30 | services.AddSingleton();
31 | services.AddSingleton();
32 | services.AddSingleton();
33 |
34 | services.AddTransient();
35 |
36 | var provider = services.BuildServiceProvider();
37 | Ioc.Default.ConfigureServices(provider);
38 |
39 | var _mainViewModel = provider.GetService();
40 | var _mainView = provider.GetService();
41 | _mainView!.DataContext = _mainViewModel;
42 |
43 | var viewLocator = new ViewLocator(provider.GetService()!);
44 | DataTemplates.Add(viewLocator);
45 |
46 | desktop.MainWindow = _mainView;
47 | }
48 |
49 | base.OnFrameworkInitializationCompleted();
50 | }
51 | }
--------------------------------------------------------------------------------
/Dialogs.Todo/Assets/avalonia-logo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevemonaco/AvaloniaDemos/2a64e0037da9902cb65729ec83ecfe3c2bef2fb9/Dialogs.Todo/Assets/avalonia-logo.ico
--------------------------------------------------------------------------------
/Dialogs.Todo/Assets/todoadditem.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevemonaco/AvaloniaDemos/2a64e0037da9902cb65729ec83ecfe3c2bef2fb9/Dialogs.Todo/Assets/todoadditem.gif
--------------------------------------------------------------------------------
/Dialogs.Todo/Assets/todoeditsaveload.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevemonaco/AvaloniaDemos/2a64e0037da9902cb65729ec83ecfe3c2bef2fb9/Dialogs.Todo/Assets/todoeditsaveload.gif
--------------------------------------------------------------------------------
/Dialogs.Todo/Converters/AppConverters.cs:
--------------------------------------------------------------------------------
1 | namespace Dialogs.Converters;
2 | public static class AppConverters
3 | {
4 | public static EnumToBooleanConverter EnumToBoolean { get; } = new();
5 | }
6 |
--------------------------------------------------------------------------------
/Dialogs.Todo/Converters/EnumToBooleanConverter.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Data.Converters;
2 | using Avalonia;
3 | using System;
4 | using System.Globalization;
5 | using Avalonia.Data;
6 |
7 | namespace Dialogs.Converters;
8 | public class EnumToBooleanConverter : IValueConverter
9 | {
10 | ///
11 | /// Matches the enum member state against the parameter
12 | ///
13 | /// The enum member value
14 | /// A boolean
15 | /// The enum member, as string or the typed enum member
16 | ///
17 | /// True if matched, false if not. UnsetValue or DoNothing if not a valid comparison
18 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
19 | {
20 | if (value is not Enum enumValue)
21 | return AvaloniaProperty.UnsetValue;
22 |
23 | if (parameter is string parameterString)
24 | {
25 | if (Enum.IsDefined(enumValue.GetType(), enumValue) == false)
26 | return BindingOperations.DoNothing;
27 |
28 | object parameterValue = Enum.Parse(enumValue.GetType(), parameterString);
29 |
30 | return parameterValue.Equals(value);
31 | }
32 |
33 | if (parameter is not null)
34 | {
35 | if (Enum.IsDefined(value.GetType(), enumValue))
36 | {
37 | return parameter.Equals(enumValue);
38 | }
39 | }
40 |
41 | return AvaloniaProperty.UnsetValue;
42 | }
43 |
44 | ///
45 | /// Returns the enum member parameter if the boolean state is true
46 | ///
47 | /// A true/false boolean value
48 | /// The enum type
49 | /// The enum member, as string or the typed enum member
50 | ///
51 | /// If true, the parameter is returned as an enum member if valid. Otherwise, UnsetValue or DoNothing
52 | public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
53 | {
54 | if (value is not bool shouldReturnEnum)
55 | return AvaloniaProperty.UnsetValue;
56 |
57 | if (!shouldReturnEnum)
58 | return BindingOperations.DoNothing;
59 |
60 | if (parameter is string enumTypeString && Enum.TryParse(targetType, enumTypeString, out var enumKind))
61 | return enumKind;
62 |
63 | if (parameter is not null && Enum.IsDefined(targetType, parameter))
64 | return parameter;
65 |
66 | return AvaloniaProperty.UnsetValue;
67 | }
68 | }
--------------------------------------------------------------------------------
/Dialogs.Todo/Dialogs.Todo.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | WinExe
4 | true
5 | app.manifest
6 | true
7 |
8 |
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 | PreserveNewest
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/Dialogs.Todo/Mappers/TodoMappers.cs:
--------------------------------------------------------------------------------
1 | using Dialogs.Models;
2 | using Dialogs.ViewModels;
3 |
4 | namespace Dialogs.Mappers;
5 | public static class TodoMappers
6 | {
7 | public static TodoModel ToModel(this TodoViewModel vm) =>
8 | new TodoModel(vm.Activity, vm.IsCompleted, vm.Priority);
9 |
10 | public static TodoViewModel ToViewModel(this TodoModel model) =>
11 | new TodoViewModel()
12 | {
13 | Activity = model.Activity,
14 | IsCompleted = model.IsCompleted,
15 | Priority = model.Priority
16 | };
17 | }
18 |
--------------------------------------------------------------------------------
/Dialogs.Todo/Models/TodoModel.cs:
--------------------------------------------------------------------------------
1 | namespace Dialogs.Models;
2 |
3 | public enum TodoPriority { Low, Medium, High, Urgent }
4 | public record TodoModel(string Activity, bool IsCompleted, TodoPriority Priority);
5 |
--------------------------------------------------------------------------------
/Dialogs.Todo/PresentationFactory.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Controls;
2 | using CommunityToolkit.Mvvm.ComponentModel;
3 | using CommunityToolkit.Mvvm.DependencyInjection;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.ComponentModel;
7 |
8 | namespace Dialogs;
9 |
10 | ///
11 | /// Creates instances of ViewModels and Views by type, along with type-based ViewModel -> View resolution
12 | ///
13 | public class PresentationFactory
14 | {
15 | private Dictionary> _locatorFactory = new();
16 |
17 | public Control ResolveView() => ResolveView(typeof(T));
18 |
19 | public Control ResolveView(INotifyPropertyChanged viewModel) => ResolveView(viewModel.GetType());
20 |
21 | private Control ResolveView(Type type)
22 | {
23 | if (_locatorFactory.TryGetValue(type, out var factory)) // Resolve by registration
24 | {
25 | return factory.Invoke();
26 | }
27 | else // Resolve by convention
28 | {
29 | var name = type.FullName!.Replace("ViewModel", "View");
30 | var viewType = Type.GetType(name);
31 |
32 | if (viewType != null)
33 | {
34 | return (Control)Activator.CreateInstance(viewType)!;
35 | }
36 | }
37 |
38 | throw new InvalidOperationException($"Could not resolve the View for '{type}'");
39 | }
40 |
41 | public TViewModel CreateViewModel() where TViewModel : ObservableObject
42 | {
43 | return Ioc.Default.GetService()!;
44 | }
45 |
46 | public Control CreateView() where TView : Control
47 | {
48 | return Activator.CreateInstance()!;
49 | }
50 |
51 | public void RegisterViewFactory()
52 | where TViewModel : class
53 | where TView : Control
54 | => _locatorFactory.Add(typeof(TViewModel), CreateView);
55 | }
56 |
--------------------------------------------------------------------------------
/Dialogs.Todo/Program.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using System;
3 |
4 | namespace Dialogs;
5 |
6 | internal class Program
7 | {
8 | // Initialization code. Don't use any Avalonia, third-party APIs or any
9 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
10 | // yet and stuff might break.
11 | [STAThread]
12 | public static void Main(string[] args) => BuildAvaloniaApp()
13 | .StartWithClassicDesktopLifetime(args);
14 |
15 | // Avalonia configuration, don't remove; also used by visual designer.
16 | public static AppBuilder BuildAvaloniaApp()
17 | => AppBuilder.Configure()
18 | .UsePlatformDetect()
19 | .WithInterFont()
20 | .LogToTrace();
21 | }
22 |
--------------------------------------------------------------------------------
/Dialogs.Todo/README.md:
--------------------------------------------------------------------------------
1 | Basic Todo app that demonstrates several types of MVVM-friendly dialogs. It implements the interfaces from [Dialogs.Abstractions](/Dialogs.Abstractions) using [FluentAvalonia](https://github.com/amwx/FluentAvalonia) for hosting dialogs in-app.
2 |
3 | ## Add Todo Item
4 |
5 | 
6 |
7 | Shows a dialog request for complex data input. Also displays a message box to prove that nested dialogs can work in this system.
8 |
9 | ## Save, Load, and Edit Todos
10 |
11 | 
12 |
13 | Shows how to use open and save file picker dialogs in an MVVM-friendly way.
14 |
15 | ## Philosophy
16 |
17 | MVVM shouldn't know about View concepts such as dialogs. Therefore, I've changed the terminology of common types to be more View-neutral. `DialogService` changes to `InteractionService` to denote some interaction with the user. Any interaction that can be cancelled by the user is a `Request`. Information passed to the user which they must acknowledge (usually with an Ok button) is called an `Alert` instead of a `MessageBox` with only an Ok button. When there are multiple choices, it's a `Prompt` instead of a `MessageBox` with Yes/No/Cancel buttons.
18 |
19 | ## Disclaimers
20 |
21 | This approach to dialogs only works well for single window desktop applications. In multiple window settings, dialog services require `TopLevel` information to determine the correct parent for the dialog.
22 |
23 | FluentAvalonia was used because it has easy integration with its `ContentDialog` and in-app overlay dialogs. An alternative would be [DialogHost.Avalonia](https://github.com/AvaloniaUtils/DialogHost.Avalonia) with some work to reimplement `ContentDialog`.
24 |
--------------------------------------------------------------------------------
/Dialogs.Todo/Services/FileRequestService.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Avalonia.Controls.ApplicationLifetimes;
3 | using Avalonia.Controls;
4 | using Avalonia.Platform.Storage;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 |
8 | namespace Dialogs.Services;
9 |
10 | ///
11 | /// View-neutral interface to request a user to select file(s)
12 | /// This is application-specific for each type of request
13 | ///
14 | public interface IFileRequestService
15 | {
16 | Task RequestLoadTodoFileName();
17 | Task RequestSaveTodoFileName();
18 | }
19 |
20 | public class FileRequestService : IFileRequestService
21 | {
22 | public async Task RequestLoadTodoFileName()
23 | {
24 | if (GetStorageProvider() is not { } storageProvider)
25 | return null;
26 |
27 | var folder = await storageProvider.TryGetFolderFromPathAsync("_data");
28 |
29 | var options = new FilePickerOpenOptions()
30 | {
31 | FileTypeFilter = new List()
32 | {
33 | new FilePickerFileType("Todos File")
34 | {
35 | Patterns = ["*.json"],
36 | AppleUniformTypeIdentifiers = ["public.json"],
37 | MimeTypes = ["application/json"]
38 | }
39 | },
40 | SuggestedStartLocation = folder,
41 | Title = "Load Todo JSON File"
42 | };
43 |
44 | var pickerResult = await storageProvider.OpenFilePickerAsync(options);
45 | return pickerResult?.FirstOrDefault()?.TryGetLocalPath();
46 | }
47 |
48 | public async Task RequestSaveTodoFileName()
49 | {
50 | if (GetStorageProvider() is not { } storageProvider)
51 | return null;
52 |
53 | var folder = await storageProvider.TryGetFolderFromPathAsync("_data");
54 |
55 | var options = new FilePickerSaveOptions()
56 | {
57 | SuggestedFileName = ".json",
58 | DefaultExtension = "json",
59 | SuggestedStartLocation = folder,
60 | Title = "Save Todo JSON File"
61 | };
62 |
63 | var pickerResult = await storageProvider.SaveFilePickerAsync(options);
64 | return pickerResult?.TryGetLocalPath();
65 | }
66 |
67 | private static IStorageProvider? GetStorageProvider()
68 | {
69 | var lifetime = Avalonia.Application.Current!.ApplicationLifetime;
70 |
71 | var root = lifetime switch
72 | {
73 | IClassicDesktopStyleApplicationLifetime desktop => desktop.MainWindow,
74 | ISingleViewApplicationLifetime single => single.MainView,
75 | _ => null
76 | };
77 |
78 | if (root is null)
79 | return null;
80 |
81 | return TopLevel.GetTopLevel(root)?.StorageProvider;
82 | }
83 | }
--------------------------------------------------------------------------------
/Dialogs.Todo/Services/TodoService.cs:
--------------------------------------------------------------------------------
1 | using Dialogs.Models;
2 | using System.Collections.Generic;
3 | using System.Text.Json;
4 |
5 | namespace Dialogs.Services;
6 | public interface ITodoService
7 | {
8 | IEnumerable? DeserializeTodos(string jsonContent);
9 | string SerializeTodos(IEnumerable items);
10 | }
11 |
12 | public class TodoService : ITodoService
13 | {
14 | private JsonSerializerOptions _options = new()
15 | {
16 | PropertyNameCaseInsensitive = true
17 | };
18 |
19 | public IEnumerable? DeserializeTodos(string jsonContent)
20 | {
21 | return JsonSerializer.Deserialize>(jsonContent, _options);
22 | }
23 |
24 | public string SerializeTodos(IEnumerable items)
25 | {
26 | return JsonSerializer.Serialize(items, _options);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Dialogs.Todo/Styles/AppStyles.axaml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
10 |
11 |
16 |
19 |
22 |
23 |
24 |
30 |
31 |
36 |
37 |
48 |
49 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/Dialogs.Todo/ViewLocator.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Controls;
2 | using Avalonia.Controls.Templates;
3 | using CommunityToolkit.Mvvm.ComponentModel;
4 |
5 | namespace Dialogs;
6 | public class ViewLocator : IDataTemplate
7 | {
8 | private readonly PresentationFactory _presentationFactory;
9 |
10 | public ViewLocator(PresentationFactory presentationFactory)
11 | {
12 | _presentationFactory = presentationFactory;
13 | }
14 |
15 | public Control Build(object? data)
16 | {
17 | if (data is null)
18 | return new TextBlock { Text = $"{nameof(data)} was null" };
19 |
20 | if (data is not ObservableObject viewModel)
21 | return new TextBlock { Text = $"{nameof(data)} is not a ViewModel" };
22 |
23 | return _presentationFactory.ResolveView(viewModel);
24 | }
25 |
26 | public bool Match(object? data) => data is ObservableObject;
27 | }
--------------------------------------------------------------------------------
/Dialogs.Todo/ViewModels/MainWindowViewModel.cs:
--------------------------------------------------------------------------------
1 | using CommunityToolkit.Mvvm.ComponentModel;
2 | using CommunityToolkit.Mvvm.Input;
3 | using Dialogs.Abstractions;
4 | using Dialogs.Mappers;
5 | using Dialogs.Models;
6 | using Dialogs.Services;
7 | using System.Collections.Generic;
8 | using System.Collections.ObjectModel;
9 | using System.IO;
10 | using System.Linq;
11 | using System.Threading.Tasks;
12 |
13 | namespace Dialogs.ViewModels;
14 |
15 | public partial class MainWindowViewModel : ObservableObject
16 | {
17 | [ObservableProperty] private ObservableCollection _todos = new()
18 | {
19 | new TodoViewModel
20 | {
21 | Activity = "Mow the Lawn",
22 | Priority = TodoPriority.Medium
23 | },
24 | new TodoViewModel
25 | {
26 | Activity = "Buy Groceries",
27 | Priority = TodoPriority.High,
28 | },
29 | new TodoViewModel
30 | {
31 | Activity = "Fix the Door Hinge",
32 | Priority = TodoPriority.Low,
33 | },
34 | new TodoViewModel
35 | {
36 | Activity = "Finish this Demo",
37 | Priority = TodoPriority.Urgent,
38 | }
39 | };
40 |
41 | public IReadOnlyList Priorities { get; } = new[] { TodoPriority.Low, TodoPriority.Medium, TodoPriority.High, TodoPriority.Urgent };
42 |
43 | private readonly ITodoService _todoService;
44 | private readonly IInteractionService _interactionService;
45 | private readonly IFileRequestService _fileRequestService;
46 |
47 | public MainWindowViewModel(ITodoService todoService, IInteractionService interactionService, IFileRequestService fileRequestService)
48 | {
49 | _todoService = todoService;
50 | _interactionService = interactionService;
51 | _fileRequestService = fileRequestService;
52 | }
53 |
54 | [RelayCommand]
55 | public async Task RequestAddTodo()
56 | {
57 | var vm = new TodoEditorViewModel(_interactionService)
58 | {
59 | Title = "Add New Todo",
60 | AcceptName = "Add"
61 | };
62 |
63 | var result = await _interactionService.RequestAsync(vm);
64 | if (result is not null)
65 | Todos.Add(result);
66 | }
67 |
68 | [RelayCommand]
69 | public void DeleteTodo(TodoViewModel todo)
70 | {
71 | Todos.Remove(todo);
72 | }
73 |
74 | [RelayCommand]
75 | public async Task RequestLoadTodos()
76 | {
77 | var filename = await _fileRequestService.RequestLoadTodoFileName();
78 |
79 | if (filename is null)
80 | return;
81 |
82 | var content = await File.ReadAllTextAsync(filename);
83 | var todos = _todoService.DeserializeTodos(content);
84 |
85 | if (todos is null)
86 | return;
87 |
88 | Todos = new(todos.Select(x => x.ToViewModel()));
89 | }
90 |
91 | [RelayCommand]
92 | public async Task RequestSaveTodos()
93 | {
94 | var filename = await _fileRequestService.RequestSaveTodoFileName();
95 |
96 | if (filename is null)
97 | return;
98 |
99 | var content = _todoService.SerializeTodos(Todos.Select(x => x.ToModel()));
100 | await File.WriteAllTextAsync(filename, content);
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/Dialogs.Todo/ViewModels/TodoCollectionViewModel.cs:
--------------------------------------------------------------------------------
1 | using CommunityToolkit.Mvvm.ComponentModel;
2 | using System.Collections.ObjectModel;
3 |
4 | namespace Dialogs.ViewModels;
5 | public partial class TodoCollectionViewModel : ObservableObject
6 | {
7 | [ObservableProperty] private ObservableCollection _todos = new();
8 |
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/Dialogs.Todo/ViewModels/TodoEditorViewModel.cs:
--------------------------------------------------------------------------------
1 | using CommunityToolkit.Mvvm.ComponentModel;
2 | using CommunityToolkit.Mvvm.Input;
3 | using Dialogs.Abstractions;
4 | using Dialogs.Models;
5 | using System.Threading.Tasks;
6 |
7 | namespace Dialogs.ViewModels;
8 | public partial class TodoEditorViewModel : DialogViewModel
9 | {
10 | [ObservableProperty] private string _activity = "";
11 | [ObservableProperty] private bool _isCompleted;
12 | [ObservableProperty] private TodoPriority _priority = TodoPriority.Low;
13 |
14 | private readonly IInteractionService _interactionService;
15 |
16 | public TodoEditorViewModel(IInteractionService interactionService)
17 | {
18 | _interactionService = interactionService;
19 | }
20 |
21 | protected override void Accept()
22 | {
23 | RequestResult = new TodoViewModel()
24 | {
25 | Activity = Activity,
26 | IsCompleted = IsCompleted,
27 | Priority = Priority
28 | };
29 | }
30 |
31 | [RelayCommand]
32 | public async Task TestAlert()
33 | {
34 | var message = $"Activity: {Activity}\nPriority: {Priority}";
35 | await _interactionService.AlertAsync("Testing Nested Dialogs", message);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Dialogs.Todo/ViewModels/TodoViewModel.cs:
--------------------------------------------------------------------------------
1 | using CommunityToolkit.Mvvm.ComponentModel;
2 | using Dialogs.Models;
3 |
4 | namespace Dialogs.ViewModels;
5 |
6 | public partial class TodoViewModel : ObservableObject
7 | {
8 | [ObservableProperty] private string _activity = "";
9 | [ObservableProperty] private bool _isCompleted;
10 | [ObservableProperty] private TodoPriority _priority = TodoPriority.Low;
11 | }
12 |
--------------------------------------------------------------------------------
/Dialogs.Todo/Views/MainWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Controls;
2 |
3 | namespace Dialogs.Views;
4 | public partial class MainWindow : Window
5 | {
6 | public MainWindow()
7 | {
8 | InitializeComponent();
9 | }
10 | }
--------------------------------------------------------------------------------
/Dialogs.Todo/Views/TodoEditorView.axaml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Dialogs.Todo/Views/TodoEditorView.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Controls;
2 |
3 | namespace Dialogs.Views;
4 |
5 | public partial class TodoEditorView : UserControl
6 | {
7 | public TodoEditorView()
8 | {
9 | InitializeComponent();
10 | }
11 | }
--------------------------------------------------------------------------------
/Dialogs.Todo/_data/default.json:
--------------------------------------------------------------------------------
1 | [{"Activity":"Mow the Lawn","IsCompleted":false,"Priority":1},{"Activity":"Buy Groceries","IsCompleted":false,"Priority":2},{"Activity":"Fix the Door Hinge","IsCompleted":false,"Priority":0},{"Activity":"Finish this Demo","IsCompleted":false,"Priority":3}]
--------------------------------------------------------------------------------
/Dialogs.Todo/app.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | net9.0
4 | 13
5 | enable
6 | true
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Directory.Packages.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | true
5 | false
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/ImageHotspot/App.axaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/ImageHotspot/App.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls.ApplicationLifetimes;
3 | using Avalonia.Markup.Xaml;
4 |
5 | namespace ImageHotspot;
6 | public partial class App : Application
7 | {
8 | public override void Initialize()
9 | {
10 | AvaloniaXamlLoader.Load(this);
11 | }
12 |
13 | public override void OnFrameworkInitializationCompleted()
14 | {
15 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
16 | {
17 | desktop.MainWindow = new MainWindow();
18 | }
19 |
20 | base.OnFrameworkInitializationCompleted();
21 | }
22 | }
--------------------------------------------------------------------------------
/ImageHotspot/Assets/controller.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevemonaco/AvaloniaDemos/2a64e0037da9902cb65729ec83ecfe3c2bef2fb9/ImageHotspot/Assets/controller.jpg
--------------------------------------------------------------------------------
/ImageHotspot/Assets/demoScreenCapture.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevemonaco/AvaloniaDemos/2a64e0037da9902cb65729ec83ecfe3c2bef2fb9/ImageHotspot/Assets/demoScreenCapture.gif
--------------------------------------------------------------------------------
/ImageHotspot/ImageHotspot.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | WinExe
4 | true
5 | app.manifest
6 | true
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/ImageHotspot/MainWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Controls;
2 | using Avalonia.Interactivity;
3 | using Avalonia.Threading;
4 | using System;
5 |
6 | namespace ImageHotspot;
7 |
8 | public enum ControllerButton { Up, Left, Right, Down, LeftAnalog, RightAnalog, Start, B, A, Y, X, LeftShoulder, RightShoulder, Z }
9 |
10 | public partial class MainWindow : Window
11 | {
12 | private DispatcherTimer _timer;
13 |
14 | public MainWindow()
15 | {
16 | InitializeComponent();
17 | _timer = new DispatcherTimer(TimeSpan.FromSeconds(1), DispatcherPriority.Default, Timer_Tick);
18 | _timer.Stop();
19 | }
20 |
21 | private void Timer_Tick(object? sender, EventArgs e)
22 | {
23 | lastPress.Text = "";
24 | _timer.Stop();
25 | }
26 |
27 | public void Controller_Click(object? sender, RoutedEventArgs e)
28 | {
29 | if (sender is not Button uiButton)
30 | return;
31 |
32 | var buttonName = uiButton.Name switch
33 | {
34 | nameof(buttonUp) => ControllerButton.Up,
35 | nameof(buttonLeft) => ControllerButton.Left,
36 | nameof(buttonRight) => ControllerButton.Right,
37 | nameof(buttonDown) => ControllerButton.Down,
38 | nameof(buttonLeftAnalog) => ControllerButton.LeftAnalog,
39 | nameof(buttonRightAnalog) => ControllerButton.RightAnalog,
40 | nameof(buttonStart) => ControllerButton.Start,
41 | nameof(buttonB) => ControllerButton.B,
42 | nameof(buttonA) => ControllerButton.A,
43 | nameof(buttonY) => ControllerButton.Y,
44 | nameof(buttonX) => ControllerButton.X,
45 | nameof(buttonL) => ControllerButton.LeftShoulder,
46 | nameof(buttonR) => ControllerButton.RightShoulder,
47 | nameof(buttonZ) => ControllerButton.Z,
48 | _ => throw new ArgumentException($"Button '{uiButton.Name}' could not be handled")
49 | };
50 |
51 | lastPress.Text = buttonName.ToString();
52 | _timer.Stop();
53 | _timer.Start();
54 | }
55 | }
--------------------------------------------------------------------------------
/ImageHotspot/Program.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using System;
3 |
4 | namespace ImageHotspot;
5 |
6 | internal class Program
7 | {
8 | // Initialization code. Don't use any Avalonia, third-party APIs or any
9 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
10 | // yet and stuff might break.
11 | [STAThread]
12 | public static void Main(string[] args) => BuildAvaloniaApp()
13 | .StartWithClassicDesktopLifetime(args);
14 |
15 | // Avalonia configuration, don't remove; also used by visual designer.
16 | public static AppBuilder BuildAvaloniaApp()
17 | => AppBuilder.Configure()
18 | .UsePlatformDetect()
19 | .WithInterFont()
20 | .LogToTrace();
21 | }
22 |
--------------------------------------------------------------------------------
/ImageHotspot/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | ImageHotspot demonstrates how to build interactive buttons onto a fixed size raster image.
4 |
5 | ## Problems and approach used:
6 |
7 | ### Image must be scalable while maintaining Button locations
8 |
9 | A Canvas matching the size of the Image is used. The Canvas contains the Image as well as all Buttons which are placed using absolute Canvas coordinates. An external image editor was used to manually determine coordinates and dimensions of the Buttons.
10 |
11 | Viewbox is used to scale the Canvas to the full window size.
12 |
13 | ### Interactive Buttons can be of arbitrary shape
14 |
15 | Ellipse and Rectangle are used for simpler shapes. Path is used as the Button child to define a shape that can have a outline and fill.
16 |
17 | Inkscape was used to trace paths around the non-square, non-circular elements. One difficulty is that in Avalonia, the Buttons are translated in the Canvas. This means that child content inherits the Button position and not the Image. In Inkscape, you can fake this by applying an SVG transform such that the SVG Path is relative to (0, 0) after tracing.
18 |
19 | ### Button hitboxes and outline should match the arbitrary shape
20 |
21 | Button.Clip allows setting of a Geometry to clip rendering and hit testing. For circular elements, EllipseGeometry is used. For Path-based elements, Inkscape was used to create an offset Path from the original, about 2px, which was set as a PathGeometry. This allowed the outline to be drawn without partial clipping.
22 |
23 | ## Alternative approaches
24 |
25 | Inkscape can create SVGs with images. It should be possible to draw paths, save as SVG, and load the SVG to create paths and visual elements from information within the SVG. Likely the better approach if you were making a stadium seating chart interactive with potentially hundreds of hotspots.
26 |
27 | ## Resources
28 |
29 | For tips on tracing into vector graphics, read [So What's the Big Deal with Horizontal & Vertical Bezier Handles Anyway?](https://www.photoshopfaceoff.com/design-tutorials/so-what-s-the-big-deal-with-horizontal-vertical-bezier-handles-anyway.html)
30 |
31 | 
32 |
33 | The controller SVG is provided for examination. Only the arbitrary shapes were drawn. The rest were determined by a traditional raster image editor.
34 |
--------------------------------------------------------------------------------
/ImageHotspot/app.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Stephen Monaco
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 |
--------------------------------------------------------------------------------
/NoSchemaCsv.Immutable/App.axaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/NoSchemaCsv.Immutable/App.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls.ApplicationLifetimes;
3 | using Avalonia.Data.Core.Plugins;
4 | using Avalonia.Markup.Xaml;
5 | using CommunityToolkit.Mvvm.DependencyInjection;
6 | using Microsoft.Extensions.DependencyInjection;
7 | using NoSchemaCsv.Immutable.Services;
8 | using NoSchemaCsv.Immutable.ViewModels;
9 | using NoSchemaCsv.Immutable.Views;
10 |
11 | namespace NoSchemaCsv.Immutable;
12 | public partial class App : Application
13 | {
14 | public override void Initialize()
15 | {
16 | AvaloniaXamlLoader.Load(this);
17 | }
18 |
19 | public override void OnFrameworkInitializationCompleted()
20 | {
21 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
22 | {
23 | BindingPlugins.DataValidators.RemoveAt(0);
24 |
25 | // Setup DI
26 | var services = new ServiceCollection();
27 | services.AddSingleton();
28 | services.AddSingleton();
29 | services.AddTransient();
30 | services.AddTransient();
31 |
32 | var provider = services.BuildServiceProvider();
33 | Ioc.Default.ConfigureServices(provider);
34 |
35 | var _mainViewModel = provider.GetService();
36 | var _mainView = provider.GetService();
37 | _mainView!.DataContext = _mainViewModel;
38 |
39 | desktop.MainWindow = _mainView;
40 | }
41 |
42 | base.OnFrameworkInitializationCompleted();
43 | }
44 | }
--------------------------------------------------------------------------------
/NoSchemaCsv.Immutable/Assets/avalonia-logo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevemonaco/AvaloniaDemos/2a64e0037da9902cb65729ec83ecfe3c2bef2fb9/NoSchemaCsv.Immutable/Assets/avalonia-logo.ico
--------------------------------------------------------------------------------
/NoSchemaCsv.Immutable/Assets/noschemacsv-immutable.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevemonaco/AvaloniaDemos/2a64e0037da9902cb65729ec83ecfe3c2bef2fb9/NoSchemaCsv.Immutable/Assets/noschemacsv-immutable.gif
--------------------------------------------------------------------------------
/NoSchemaCsv.Immutable/Messages.cs:
--------------------------------------------------------------------------------
1 | namespace NoSchemaCsv.Immutable;
2 |
3 | public record CsvChangedMessage();
--------------------------------------------------------------------------------
/NoSchemaCsv.Immutable/NoSchemaCsv.Immutable.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | WinExe
4 | true
5 | app.manifest
6 | true
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/NoSchemaCsv.Immutable/Program.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using System;
3 |
4 | namespace NoSchemaCsv.Immutable;
5 |
6 | internal sealed class Program
7 | {
8 | // Initialization code. Don't use any Avalonia, third-party APIs or any
9 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
10 | // yet and stuff might break.
11 | [STAThread]
12 | public static void Main(string[] args) => BuildAvaloniaApp()
13 | .StartWithClassicDesktopLifetime(args);
14 |
15 | // Avalonia configuration, don't remove; also used by visual designer.
16 | public static AppBuilder BuildAvaloniaApp()
17 | => AppBuilder.Configure()
18 | .UsePlatformDetect()
19 | .WithInterFont()
20 | .LogToTrace();
21 | }
22 |
--------------------------------------------------------------------------------
/NoSchemaCsv.Immutable/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | Demonstrates how to load CSV data without a known schema and display it within a `TreeDataGrid`.
4 |
5 | ### Major Concepts
6 |
7 | * Defines a `CsvRecord` type backed by an array (`IReadOnlyList`) for the unknown columns.
8 | * Creates columns for `TreeDataGrid` and uses a lambda to index the array in `CsvRecord`.
9 | * [Bogus](https://github.com/bchavez/Bogus) to generate random data for a CSV.
10 | * [CsvHelper](https://github.com/JoshClose/CsvHelper) to read back the generated CSV.
11 | * Basic DI container usage.
12 |
13 | ### Limitations
14 |
15 | * The app is intended to be a viewer and the data is presumed to be immutable.
16 | * No editing means that the loading process is less complex.
17 | * Lack of column type detection means that numeric formatting can't be applied.
18 | * Generating random data sets is slow and shouldn't be an indicator of real-world performance.
19 |
--------------------------------------------------------------------------------
/NoSchemaCsv.Immutable/Services/CsvGeneratorService.cs:
--------------------------------------------------------------------------------
1 | using Bogus;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 |
7 | namespace NoSchemaCsv.Immutable.Services;
8 | public class CsvGeneratorService
9 | {
10 | private List> _generators = [];
11 | private List _columnNames = [];
12 | private double _threshold = 0.5;
13 |
14 | ///
15 | /// Generates CSV with a random assortment of randomized fields
16 | ///
17 | /// Number of records to generate
18 | /// A CSV string content with column names and then the records
19 | public string GenerateCsv(int recordsToGenerate)
20 | {
21 | _generators = [];
22 | _columnNames = [];
23 |
24 | List