├── .idea
└── .idea.HeaderedScrollViewer
│ └── .idea
│ ├── avalonia.xml
│ ├── encodings.xml
│ └── indexLayout.xml
├── HeaderedScrollViewer.sln
├── HeaderedScrollViewer
├── App.axaml
├── App.axaml.cs
├── Assets
│ └── avalonia-logo.ico
├── HeaderedScrollViewer.csproj
├── Program.cs
├── ViewLocator.cs
├── ViewModels
│ ├── MainWindowViewModel.cs
│ └── ViewModelBase.cs
└── Views
│ ├── HeaderedScrollViewer.cs
│ ├── MainWindow.axaml
│ └── MainWindow.axaml.cs
└── README.md
/.idea/.idea.HeaderedScrollViewer/.idea/avalonia.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/.idea.HeaderedScrollViewer/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/.idea.HeaderedScrollViewer/.idea/indexLayout.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/HeaderedScrollViewer.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HeaderedScrollViewer", "HeaderedScrollViewer\HeaderedScrollViewer.csproj", "{18DEEDE7-69FF-4C14-8A8E-62F321410B88}"
4 | EndProject
5 | Global
6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
7 | Debug|Any CPU = Debug|Any CPU
8 | Release|Any CPU = Release|Any CPU
9 | EndGlobalSection
10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
11 | {18DEEDE7-69FF-4C14-8A8E-62F321410B88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
12 | {18DEEDE7-69FF-4C14-8A8E-62F321410B88}.Debug|Any CPU.Build.0 = Debug|Any CPU
13 | {18DEEDE7-69FF-4C14-8A8E-62F321410B88}.Release|Any CPU.ActiveCfg = Release|Any CPU
14 | {18DEEDE7-69FF-4C14-8A8E-62F321410B88}.Release|Any CPU.Build.0 = Release|Any CPU
15 | EndGlobalSection
16 | EndGlobal
17 |
--------------------------------------------------------------------------------
/HeaderedScrollViewer/App.axaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/HeaderedScrollViewer/App.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls.ApplicationLifetimes;
3 | using Avalonia.Markup.Xaml;
4 | using HeaderedScrollViewer.ViewModels;
5 | using HeaderedScrollViewer.Views;
6 |
7 | namespace HeaderedScrollViewer
8 | {
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 | desktop.MainWindow = new MainWindow
21 | {
22 | DataContext = new MainWindowViewModel(),
23 | };
24 | }
25 |
26 | base.OnFrameworkInitializationCompleted();
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/HeaderedScrollViewer/Assets/avalonia-logo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stripe2933/HeaderedScrollViewer/6ec483d72771e8f689d73dc11596c1a361f367b1/HeaderedScrollViewer/Assets/avalonia-logo.ico
--------------------------------------------------------------------------------
/HeaderedScrollViewer/HeaderedScrollViewer.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | WinExe
4 | net6.0
5 | enable
6 |
7 | copyused
8 | true
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/HeaderedScrollViewer/Program.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.ReactiveUI;
3 | using System;
4 |
5 | namespace HeaderedScrollViewer
6 | {
7 | class Program
8 | {
9 | // Initialization code. Don't use any Avalonia, third-party APIs or any
10 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
11 | // yet and stuff might break.
12 | [STAThread]
13 | public static void Main(string[] args) => BuildAvaloniaApp()
14 | .StartWithClassicDesktopLifetime(args);
15 |
16 | // Avalonia configuration, don't remove; also used by visual designer.
17 | public static AppBuilder BuildAvaloniaApp()
18 | => AppBuilder.Configure()
19 | .UsePlatformDetect()
20 | .LogToTrace()
21 | .UseReactiveUI();
22 | }
23 | }
--------------------------------------------------------------------------------
/HeaderedScrollViewer/ViewLocator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Avalonia.Controls;
3 | using Avalonia.Controls.Templates;
4 | using HeaderedScrollViewer.ViewModels;
5 |
6 | namespace HeaderedScrollViewer
7 | {
8 | public class ViewLocator : IDataTemplate
9 | {
10 | public IControl Build(object data)
11 | {
12 | var name = data.GetType().FullName!.Replace("ViewModel", "View");
13 | var type = Type.GetType(name);
14 |
15 | if (type != null)
16 | {
17 | return (Control)Activator.CreateInstance(type)!;
18 | }
19 |
20 | return new TextBlock { Text = "Not Found: " + name };
21 | }
22 |
23 | public bool Match(object data)
24 | {
25 | return data is ViewModelBase;
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/HeaderedScrollViewer/ViewModels/MainWindowViewModel.cs:
--------------------------------------------------------------------------------
1 | using ReactiveUI;
2 |
3 | namespace HeaderedScrollViewer.ViewModels
4 | {
5 | public class MainWindowViewModel : ViewModelBase
6 | {
7 |
8 | }
9 | }
--------------------------------------------------------------------------------
/HeaderedScrollViewer/ViewModels/ViewModelBase.cs:
--------------------------------------------------------------------------------
1 | using ReactiveUI;
2 |
3 | namespace HeaderedScrollViewer.ViewModels
4 | {
5 | public class ViewModelBase : ReactiveObject
6 | {
7 | }
8 | }
--------------------------------------------------------------------------------
/HeaderedScrollViewer/Views/HeaderedScrollViewer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using Avalonia;
5 | using Avalonia.Controls;
6 |
7 | namespace HeaderedScrollViewer.Views;
8 |
9 | public class HeaderedScrollViewer : AvaloniaObject
10 | {
11 | #region Fields
12 |
13 | private static List? _headerTexts;
14 |
15 | #endregion
16 |
17 | #region Avalonia Properties
18 |
19 | ///
20 | /// The classname to mark the TextBlock as header text.
21 | ///
22 | public static readonly AttachedProperty TargetHeaderProperty =
23 | AvaloniaProperty.RegisterAttached("TargetHeader");
24 | public static void SetTargetHeader(AvaloniaObject element, string value) =>
25 | element.SetValue(TargetHeaderProperty, value);
26 | public static string GetTargetHeader(AvaloniaObject element) =>
27 | element.GetValue(TargetHeaderProperty);
28 |
29 | ///
30 | /// Current header text; null if no current header text
31 | ///
32 | public static readonly AttachedProperty CurrentHeaderTextProperty =
33 | AvaloniaProperty.RegisterAttached("CurrentHeaderText");
34 | public static void SetCurrentHeaderText(AvaloniaObject element, string? value) =>
35 | element.SetValue(CurrentHeaderTextProperty, value);
36 | public static string? GetCurrentHeaderText(AvaloniaObject element) =>
37 | element.GetValue(CurrentHeaderTextProperty);
38 |
39 | #endregion
40 |
41 | static HeaderedScrollViewer()
42 | {
43 | TargetHeaderProperty.Changed.Subscribe(x =>
44 | HandleTargetHeaderChanged(x.Sender, x.NewValue.GetValueOrDefault() ?? string.Empty));
45 | }
46 |
47 | ///
48 | /// Get TextBlocks which are marked as header text.
49 | ///
50 | /// A ScrollViewer that has StackPanel for its contents and the StackPanel has headers inside.
51 | /// Classname for header text mark.
52 | /// A lists of Header TextBlock. If nothing found or scrollViewer does not have StackPanel for its content, return null.
53 | private static IEnumerable? GetHeaderTextBlocks(ScrollViewer scrollViewer, string targetHeader)
54 | {
55 | if (scrollViewer.Content is StackPanel stackPanel)
56 | {
57 | return stackPanel.Children
58 | .Where(p => p is TextBlock textBlock && textBlock.Classes.Contains(targetHeader))
59 | .Cast();
60 | }
61 |
62 | return null;
63 | }
64 |
65 |
66 | private static void HandleTargetHeaderChanged(IAvaloniaObject element, string value)
67 | {
68 | if (element is ScrollViewer scrollViewer)
69 | {
70 | if (!string.IsNullOrEmpty(value))
71 | {
72 | // Add non-null value
73 | scrollViewer.AddHandler(ScrollViewer.ScrollChangedEvent, Handler);
74 | }
75 | else
76 | {
77 | // remove prev value
78 | scrollViewer.RemoveHandler(ScrollViewer.ScrollChangedEvent, Handler);
79 | }
80 | }
81 |
82 | // Executed when the ScrollViewer scroll changed.
83 | void Handler(object? s, ScrollChangedEventArgs e)
84 | {
85 | // Get header TextBlocks in scrollViewer if _headerTexts is null
86 | if (_headerTexts == null)
87 | {
88 | var textEnumerables = GetHeaderTextBlocks(scrollViewer, value);
89 | _headerTexts = textEnumerables != null ? new List(textEnumerables) : null;
90 | }
91 |
92 | // Find the current header TextBlock which meets the condition.
93 | var currentHeaderTextBlock = _headerTexts?.LastOrDefault(p =>
94 | {
95 | var transform = p.TransformToVisual(scrollViewer);
96 | double y = new Point().Transform(transform ?? Matrix.Identity).Y;
97 |
98 | return y <= -p.Bounds.Height;
99 | });
100 | string? currentHeaderText = currentHeaderTextBlock?.Text;
101 |
102 | SetCurrentHeaderText(scrollViewer, currentHeaderText);
103 | }
104 | }
105 | }
--------------------------------------------------------------------------------
/HeaderedScrollViewer/Views/MainWindow.axaml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
18 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
34 |
35 |
38 |
39 |
40 |
41 |
42 | Header 2
43 |
44 | Header 2 Item - 1
45 | Header 2 Item - 2
46 | Header 2 Item - 3
47 |
48 |
49 | Header 3
50 |
51 |
52 | Header 4
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/HeaderedScrollViewer/Views/MainWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Controls;
2 |
3 | namespace HeaderedScrollViewer.Views
4 | {
5 | public partial class MainWindow : Window
6 | {
7 | public MainWindow()
8 | {
9 | InitializeComponent();
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # HeaderedScrollViewer
2 |
3 | 
4 |
5 | Avalonia Attached Property for showing current header in the ScrollViewer.
6 |
7 | ## How to use
8 |
9 | 1. Add the file `HeaderedScrollViewer.cs` file into your project.
10 | 2. Make element hierarchy as:
11 | - ScrollViewer
12 | - StackPanel (Orientation="Vertical")
13 | - // TextBlock (Classes="header") (*header 1) (the comment is intended)
14 | - Content for header 1
15 | - TextBlock (Classes="header") (*header 2)
16 | - Content for header 2
17 | - TextBlock (Classes="header") (*header 3)
18 | - Content for header 3
19 |
20 | ```xaml
21 |
24 |
25 |
26 |
27 |
28 | Header 2
29 |
30 | Header 2 Item - 1
31 | Header 2 Item - 2
32 | Header 2 Item - 3
33 |
34 |
35 | Header 3
36 |
37 |
38 | Header 4
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | ```
47 |
48 | 3. Attach `HeaderedScrollViewer` to the ScrollViewer and set its TargetHeader as the classname that your marked your header TextBlock. I set the classname as `header`.
49 |
50 | ```xaml
51 | ...
52 | ```
53 |
54 | 4. Use `CurrentHeaderText` property in other controls, such as
55 | ```xaml
56 |
57 | ```
58 |
59 | ## Example
60 |
61 | The source code is in solution file.
62 |
63 | `HeaderedScrollViewer.cs` (inside `View` folder)
64 |
65 | `MainWindow.xaml` (inside `View` folder)
66 | ```xaml
67 |
77 |
78 |
82 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
98 |
99 |
103 |
104 |
105 |
106 | Header 2
107 |
108 | Header 2 Item - 1
109 | Header 2 Item - 2
110 | Header 2 Item - 3
111 |
112 |
113 | Header 3
114 |
115 |
116 | Header 4
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 | ```
128 |
129 | The result is same as the above GIF file.
130 |
131 | ### Usage Example
132 |
133 | [Todo.Avalonia](https://github.com/stripe2933/Todo.Avalonia)
134 | 
135 |
--------------------------------------------------------------------------------