├── .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 | ![A working screenshot](https://user-images.githubusercontent.com/63503910/179910832-e302c691-2809-4c33-ab40-90e490133da3.gif) 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 | ![Todo.Avalonia](https://user-images.githubusercontent.com/63503910/180939386-40ff8574-cfb2-4260-9821-e0ccfa9a700f.gif) 135 | --------------------------------------------------------------------------------