Browse Source

Started rewriting VirtualizationDemo.

To give some more realistic examples of lists to test virtualization on. Started with a chatgpt-generated chat.
Steven Kirk 2 years ago
parent
commit
5e961ff7bf

+ 14 - 0
samples/VirtualizationDemo/App.axaml

@@ -0,0 +1,14 @@
+<Application xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             x:Class="VirtualizationDemo.App">
+  <Application.Styles>
+    <FluentTheme/>
+  </Application.Styles>
+  <Application.Resources>
+    <ResourceDictionary>
+      <ResourceDictionary.MergedDictionaries>
+        <ResourceInclude Source="avares://ControlSamples/HamburgerMenu/HamburgerMenu.xaml" />
+      </ResourceDictionary.MergedDictionaries>
+    </ResourceDictionary>
+  </Application.Resources>
+</Application>

+ 20 - 0
samples/VirtualizationDemo/App.axaml.cs

@@ -0,0 +1,20 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+
+namespace VirtualizationDemo;
+
+public partial class App : Application
+{
+    public override void Initialize()
+    {
+        AvaloniaXamlLoader.Load(this);
+    }
+
+    public override void OnFrameworkInitializationCompleted()
+    {
+        if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+            desktop.MainWindow = new MainWindow();
+        base.OnFrameworkInitializationCompleted();
+    }
+}

+ 0 - 7
samples/VirtualizationDemo/App.xaml

@@ -1,7 +0,0 @@
-<Application xmlns="https://github.com/avaloniaui"
-             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-             x:Class="VirtualizationDemo.App">
-  <Application.Styles>
-    <SimpleTheme />
-  </Application.Styles>
-</Application>

+ 0 - 21
samples/VirtualizationDemo/App.xaml.cs

@@ -1,21 +0,0 @@
-using Avalonia;
-using Avalonia.Controls.ApplicationLifetimes;
-using Avalonia.Markup.Xaml;
-
-namespace VirtualizationDemo
-{
-    public class App : Application
-    {
-        public override void Initialize()
-        {
-            AvaloniaXamlLoader.Load(this);
-        }
-
-        public override void OnFrameworkInitializationCompleted()
-        {
-            if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
-                desktop.MainWindow = new MainWindow();
-            base.OnFrameworkInitializationCompleted();
-        }
-    }
-}

+ 190 - 0
samples/VirtualizationDemo/Assets/chat.json

@@ -0,0 +1,190 @@
+{
+  "chat": [
+    {
+      "sender": "Alice",
+      "message": "Hey Bob! How was your weekend?",
+      "timestamp": "2023-04-01T10:00:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "It was great, thanks for asking. I went on a camping trip with some friends. How about you?",
+      "timestamp": "2023-04-01T10:01:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "My weekend was pretty chill. I just stayed home and caught up on some TV shows.",
+      "timestamp": "2023-04-01T10:03:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "That sounds relaxing. What shows did you watch?",
+      "timestamp": "2023-04-01T10:05:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "I watched the new season of 'Stranger Things' and started watching 'Ozark'. Have you seen them?",
+      "timestamp": "2023-04-01T10:07:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "Yeah, I've seen both of those. They're really good! What do you think of them so far?",
+      "timestamp": "2023-04-01T10:10:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "I'm really enjoying 'Stranger Things', but 'Ozark' is a bit darker than I expected. I'm only a few episodes in though, so we'll see how it goes.",
+      "timestamp": "2023-04-01T10:12:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "Yeah, 'Ozark' can be intense at times, but it's really well done. Keep watching, it gets even better.",
+      "timestamp": "2023-04-01T10:15:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "Thanks for the recommendation, I'll definitely keep watching. So, how's work been for you lately?",
+      "timestamp": "2023-04-01T10:20:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "It's been pretty busy, but I'm managing. How about you?",
+      "timestamp": "2023-04-01T10:22:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "Same here, things have been pretty hectic. But it keeps us on our toes, right?",
+      "timestamp": "2023-04-01T10:25:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "Absolutely. Hey, have you heard about the new project we're starting next week?",
+      "timestamp": "2023-04-01T10:30:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "No, I haven't. What's it about?",
+      "timestamp": "2023-04-01T10:32:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "It's a big project for a new client, and it's going to require a lot of extra hours from all of us. But the pay is going to be great,so it's definitely worth the extra effort. I'll fill you in on the details later, but for now, let's just enjoy our coffee break, shall we?",
+      "timestamp": "2023-04-01T10:35:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "Sounds good to me. I could use a break right about now.",
+      "timestamp": "2023-04-01T10:40:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "Me too. So, have you tried the new caf� down the street yet?",
+      "timestamp": "2023-04-01T10:45:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "No, I haven't. Is it any good?",
+      "timestamp": "2023-04-01T10:47:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "It's really good! They have the best croissants I've ever tasted.",
+      "timestamp": "2023-04-01T10:50:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "Hmm, I'll have to try it out sometime. Do they have any vegan options?",
+      "timestamp": "2023-04-01T10:52:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "I'm not sure, but I think they do. You should ask them the next time you go there.",
+      "timestamp": "2023-04-01T10:55:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "Thanks for the suggestion. I'm always looking for good vegan options around here.",
+      "timestamp": "2023-04-01T11:00:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "No problem. So, have you made any plans for the weekend yet?",
+      "timestamp": "2023-04-01T11:05:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "Not yet. I was thinking of maybe going for a hike or something. What about you?",
+      "timestamp": "2023-04-01T11:07:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "I haven't made any plans either. Maybe we could do something together?",
+      "timestamp": "2023-04-01T11:10:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "That sounds like a great idea! Let's plan on it.",
+      "timestamp": "2023-04-01T11:12:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "Awesome. I'll check out some hiking trails and let you know which ones look good.",
+      "timestamp": "2023-04-01T11:15:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "Sounds good. I can't wait!",
+      "timestamp": "2023-04-01T11:20:00"
+    },
+    {
+      "sender": "John",
+      "message": "Hey Lisa, how was your day?",
+      "timestamp": "2023-04-01T18:00:00"
+    },
+    {
+      "sender": "Lisa",
+      "message": "It was good, thanks for asking. How about you?",
+      "timestamp": "2023-04-01T18:05:00"
+    },
+    {
+      "sender": "John",
+      "message": "Eh, it was alright. Work was pretty busy, but nothing too crazy.",
+      "timestamp": "2023-04-01T18:10:00"
+    },
+    {
+      "sender": "Lisa",
+      "message": "Yeah, I know what you mean. My boss has been on my case lately about meeting our deadlines.",
+      "timestamp": "2023-04-01T18:15:00"
+    },
+    {
+      "sender": "John",
+      "message": "That sucks. Are you feeling stressed out?",
+      "timestamp": "2023-04-01T18:20:00"
+    },
+    {
+      "sender": "Lisa",
+      "message": "A little bit, yeah. But I'm trying to stay positive and focus on getting my work done.",
+      "timestamp": "2023-04-01T18:25:00"
+    },
+    {
+      "sender": "John",
+      "message": "That's a good attitude to have. Have you tried doing some meditation or other relaxation techniques?",
+      "timestamp": "2023-04-01T18:30:00"
+    },
+    {
+      "sender": "Lisa",
+      "message": "I haven't, but I've been thinking about it. Do you have any suggestions?",
+      "timestamp": "2023-04-01T18:35:00"
+    },
+    {
+      "sender": "John",
+      "message": "Sure, I could send you some links to guided meditations that I've found helpful. And there are also some great apps out there that can help you with relaxation.",
+      "timestamp": "2023-04-01T18:40:00"
+    },
+    {
+      "sender": "Lisa",
+      "message": "That would be awesome, thanks so much!",
+      "timestamp": "2023-04-01T18:45:00"
+    }
+  ]
+}
+

+ 16 - 0
samples/VirtualizationDemo/MainWindow.axaml

@@ -0,0 +1,16 @@
+<Window xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:controls="using:ControlSamples"
+        xmlns:vm="using:VirtualizationDemo.ViewModels"
+        xmlns:views="using:VirtualizationDemo.Views"
+        x:Class="VirtualizationDemo.MainWindow"
+        Title="AvaloniaUI Virtualization Demo"
+        Width="800"
+        Height="600"
+        x:DataType="vm:MainWindowViewModel">
+  <controls:HamburgerMenu>
+    <TabItem Header="Chat" >
+      <views:ChatPageView DataContext="{Binding Chat}"/>
+    </TabItem>
+  </controls:HamburgerMenu>
+</Window>

+ 15 - 0
samples/VirtualizationDemo/MainWindow.axaml.cs

@@ -0,0 +1,15 @@
+using Avalonia;
+using Avalonia.Controls;
+using VirtualizationDemo.ViewModels;
+
+namespace VirtualizationDemo;
+
+public partial class MainWindow : Window
+{
+    public MainWindow()
+    {
+        InitializeComponent();
+        this.AttachDevTools();
+        DataContext = new MainWindowViewModel();
+    }
+}

+ 0 - 64
samples/VirtualizationDemo/MainWindow.xaml

@@ -1,64 +0,0 @@
-<Window xmlns="https://github.com/avaloniaui"
-        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-        xmlns:viewModels="using:VirtualizationDemo.ViewModels"
-        x:Class="VirtualizationDemo.MainWindow"
-        Title="AvaloniaUI Virtualization Test"
-        Width="800"
-        Height="600"
-        x:DataType="viewModels:MainWindowViewModel">
-    <DockPanel LastChildFill="True" Margin="16">
-        <StackPanel DockPanel.Dock="Right" 
-                    Margin="16 0 0 0" 
-                    Width="150"
-                    Spacing="4">
-            <ComboBox ItemsSource="{Binding Orientations}"
-                      SelectedItem="{Binding Orientation}"/>
-            <TextBox Watermark="Item Count"
-                     UseFloatingWatermark="True"
-                     Text="{Binding ItemCount}"/>
-            <TextBox Watermark="Extent"
-                     UseFloatingWatermark="True"
-                     Text="{Binding #listBox.Scroll.Extent, Mode=OneWay}"/>
-            <TextBox Watermark="Offset"
-                     UseFloatingWatermark="True"
-                     Text="{Binding #listBox.Scroll.Offset, Mode=OneWay}"/>
-            <TextBox Watermark="Viewport"
-                     UseFloatingWatermark="True"
-                     Text="{Binding #listBox.Scroll.Viewport, Mode=OneWay}"/>
-            <TextBlock>Horiz. ScrollBar</TextBlock>
-            <ComboBox ItemsSource="{Binding ScrollBarVisibilities}"
-                      SelectedItem="{Binding HorizontalScrollBarVisibility}"/>
-            <TextBlock>Vert. ScrollBar</TextBlock>
-            <ComboBox ItemsSource="{Binding ScrollBarVisibilities}"
-                      SelectedItem="{Binding VerticalScrollBarVisibility}"/>
-            <TextBox Watermark="Item to Create"
-                     UseFloatingWatermark="True"
-                     Text="{Binding NewItemString}"/>
-            <Button Command="{Binding AddItemCommand}">Add Item</Button>
-            <Button Command="{Binding RemoveItemCommand}">Remove Item</Button>
-            <Button Command="{Binding RecreateCommand}">Recreate</Button>
-            <Button Command="{Binding SelectFirstCommand}">Select First</Button>
-            <Button Command="{Binding SelectLastCommand}">Select Last</Button>
-            <Button Command="{Binding RandomizeSize}">Randomize Size</Button>
-            <Button Command="{Binding ResetSize}">Reset Size</Button>
-        </StackPanel>
-
-        <ListBox Name="listBox" 
-                 ItemsSource="{Binding Items}" 
-                 Selection="{Binding Selection}"
-                 SelectionMode="Multiple"
-                 ScrollViewer.HorizontalScrollBarVisibility="{Binding HorizontalScrollBarVisibility, Mode=TwoWay}"
-                 ScrollViewer.VerticalScrollBarVisibility="{Binding VerticalScrollBarVisibility, Mode=TwoWay}">
-            <ListBox.ItemsPanel>
-                <ItemsPanelTemplate>
-                    <VirtualizingStackPanel Orientation="{Binding Orientation}"/>
-                </ItemsPanelTemplate>
-            </ListBox.ItemsPanel>
-            <ListBox.ItemTemplate>
-                <DataTemplate>
-                    <TextBlock Text="{Binding Header}" Height="{Binding Height}" TextWrapping="Wrap"/>
-                </DataTemplate>
-            </ListBox.ItemTemplate>
-        </ListBox>
-    </DockPanel>
-</Window>

+ 0 - 22
samples/VirtualizationDemo/MainWindow.xaml.cs

@@ -1,22 +0,0 @@
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Markup.Xaml;
-using VirtualizationDemo.ViewModels;
-
-namespace VirtualizationDemo
-{
-    public class MainWindow : Window
-    {
-        public MainWindow()
-        {
-            this.InitializeComponent();
-            this.AttachDevTools();
-            DataContext = new MainWindowViewModel();
-        }
-
-        private void InitializeComponent()
-        {
-            AvaloniaXamlLoader.Load(this);
-        }
-    }
-}

+ 23 - 0
samples/VirtualizationDemo/Models/Chat.cs

@@ -0,0 +1,23 @@
+using System;
+using System.IO;
+using System.Text.Json;
+
+namespace VirtualizationDemo.Models;
+
+public class ChatFile
+{
+    public ChatMessage[] Chat { get; set; }
+
+    public static ChatFile Load(string path)
+    {
+        var options = new JsonSerializerOptions
+        {
+            PropertyNameCaseInsensitive = true
+        };
+
+        using var s = File.OpenRead(path);
+        return JsonSerializer.Deserialize<ChatFile>(s, options);
+    }
+}
+
+public record ChatMessage(string Sender, string Message, DateTimeOffset Timestamp);

+ 9 - 10
samples/VirtualizationDemo/Program.cs

@@ -1,15 +1,14 @@
 using Avalonia;
 using Avalonia;
 
 
-namespace VirtualizationDemo
+namespace VirtualizationDemo;
+
+class Program
 {
 {
-    class Program
-    {
-        public static AppBuilder BuildAvaloniaApp()
-            => AppBuilder.Configure<App>()
-                .UsePlatformDetect()
-                .LogToTrace();
+    public static AppBuilder BuildAvaloniaApp()
+        => AppBuilder.Configure<App>()
+            .UsePlatformDetect()
+            .LogToTrace();
 
 
-        public static int Main(string[] args)
-            => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
-    }
+    public static int Main(string[] args)
+        => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
 }
 }

+ 16 - 0
samples/VirtualizationDemo/ViewModels/ChatPageViewModel.cs

@@ -0,0 +1,16 @@
+using System.Collections.ObjectModel;
+using System.IO;
+using VirtualizationDemo.Models;
+
+namespace VirtualizationDemo.ViewModels;
+
+public class ChatPageViewModel
+{
+    public ChatPageViewModel()
+    {
+        var chat = ChatFile.Load(Path.Combine("Assets", "chat.json"));
+        Messages = new(chat.Chat);
+    }
+
+    public ObservableCollection<ChatMessage> Messages { get; }
+}

+ 0 - 26
samples/VirtualizationDemo/ViewModels/ItemViewModel.cs

@@ -1,26 +0,0 @@
-using System;
-using MiniMvvm;
-
-namespace VirtualizationDemo.ViewModels
-{
-    internal class ItemViewModel : ViewModelBase
-    {
-        private string _prefix;
-        private int _index;
-        private double _height = double.NaN;
-
-        public ItemViewModel(int index, string prefix = "Item")
-        {
-            _prefix = prefix;
-            _index = index;
-        }
-
-        public string Header => $"{_prefix} {_index}";
-
-        public double Height
-        {
-            get => _height;
-            set => this.RaiseAndSetIfChanged(ref _height, value);
-        }
-    }
-}

+ 5 - 157
samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs

@@ -1,160 +1,8 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reactive;
-using Avalonia.Collections;
-using Avalonia.Controls;
-using Avalonia.Controls.Primitives;
-using Avalonia.Layout;
-using Avalonia.Controls.Selection;
-using MiniMvvm;
+using MiniMvvm;
 
 
-namespace VirtualizationDemo.ViewModels
-{
-    internal class MainWindowViewModel : ViewModelBase
-    {
-        private int _itemCount = 200;
-        private string _newItemString = "New Item";
-        private int _newItemIndex;
-        private AvaloniaList<ItemViewModel> _items;
-        private string _prefix = "Item";
-        private ScrollBarVisibility _horizontalScrollBarVisibility = ScrollBarVisibility.Auto;
-        private ScrollBarVisibility _verticalScrollBarVisibility = ScrollBarVisibility.Auto;
-        private Orientation _orientation = Orientation.Vertical;
-
-        public MainWindowViewModel()
-        {
-            this.WhenAnyValue(x => x.ItemCount).Subscribe(ResizeItems);
-            RecreateCommand = MiniCommand.Create(() => Recreate());
-
-            AddItemCommand = MiniCommand.Create(() => AddItem());
-
-            RemoveItemCommand = MiniCommand.Create(() => Remove());
-
-            SelectFirstCommand = MiniCommand.Create(() => SelectItem(0));
-
-            SelectLastCommand = MiniCommand.Create(() => SelectItem(Items.Count - 1));
-        }
-
-        public string NewItemString
-        {
-            get { return _newItemString; }
-            set { this.RaiseAndSetIfChanged(ref _newItemString, value); }
-        }
-
-        public int ItemCount
-        {
-            get { return _itemCount; }
-            set { this.RaiseAndSetIfChanged(ref _itemCount, value); }
-        }
-
-        public SelectionModel<ItemViewModel> Selection { get; } = new SelectionModel<ItemViewModel>();
-
-        public AvaloniaList<ItemViewModel> Items
-        {
-            get { return _items; }
-            private set { this.RaiseAndSetIfChanged(ref _items, value); }
-        }
-
-        public Orientation Orientation
-        {
-            get { return _orientation; }
-            set { this.RaiseAndSetIfChanged(ref _orientation, value); }
-        }
-
-        public IEnumerable<Orientation> Orientations =>
-            Enum.GetValues(typeof(Orientation)).Cast<Orientation>();
-
-        public ScrollBarVisibility HorizontalScrollBarVisibility
-        {
-            get { return _horizontalScrollBarVisibility; }
-            set { this.RaiseAndSetIfChanged(ref _horizontalScrollBarVisibility, value); }
-        }
+namespace VirtualizationDemo.ViewModels;
 
 
-        public ScrollBarVisibility VerticalScrollBarVisibility
-        {
-            get { return _verticalScrollBarVisibility; }
-            set { this.RaiseAndSetIfChanged(ref _verticalScrollBarVisibility, value); }
-        }
-
-        public IEnumerable<ScrollBarVisibility> ScrollBarVisibilities =>
-            Enum.GetValues(typeof(ScrollBarVisibility)).Cast<ScrollBarVisibility>();
-
-        public MiniCommand AddItemCommand { get; private set; }
-        public MiniCommand RecreateCommand { get; private set; }
-        public MiniCommand RemoveItemCommand { get; private set; }
-        public MiniCommand SelectFirstCommand { get; private set; }
-        public MiniCommand SelectLastCommand { get; private set; }
-
-        public void RandomizeSize()
-        {
-            var random = new Random();
-
-            foreach (var i in Items)
-            {
-                i.Height = random.Next(240) + 10;
-            }
-        }
-
-        public void ResetSize()
-        {
-            foreach (var i in Items)
-            {
-                i.Height = double.NaN;
-            }
-        }
-
-        private void ResizeItems(int count)
-        {
-            if (Items == null)
-            {
-                var items = Enumerable.Range(0, count)
-                    .Select(x => new ItemViewModel(x));
-                Items = new AvaloniaList<ItemViewModel>(items);
-            }
-            else if (count > Items.Count)
-            {
-                var items = Enumerable.Range(Items.Count, count - Items.Count)
-                    .Select(x => new ItemViewModel(x));
-                Items.AddRange(items);
-            }
-            else if (count < Items.Count)
-            {
-                Items.RemoveRange(count, Items.Count - count);
-            }
-        }
-
-        private void AddItem()
-        {
-            var index = Items.Count;
-
-            if (Selection.SelectedItems.Count > 0)
-            {
-                index = Selection.SelectedIndex;
-            }
-
-            Items.Insert(index, new ItemViewModel(_newItemIndex++, NewItemString));
-        }
-
-        private void Remove()
-        {
-            if (Selection.SelectedItems.Count > 0)
-            {
-                Items.RemoveAll(Selection.SelectedItems.ToList());
-            }
-        }
-
-        private void Recreate()
-        {
-            _prefix = _prefix == "Item" ? "Recreated" : "Item";
-            var items = Enumerable.Range(0, _itemCount)
-                .Select(x => new ItemViewModel(x, _prefix));
-            Items = new AvaloniaList<ItemViewModel>(items);
-        }
-
-        private void SelectItem(int index)
-        {
-            Selection.SelectedIndex = index;
-        }
-    }
+internal class MainWindowViewModel : ViewModelBase
+{
+    public ChatPageViewModel Chat { get; } = new();
 }
 }

+ 39 - 0
samples/VirtualizationDemo/Views/ChatPageView.axaml

@@ -0,0 +1,39 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             xmlns:vm="using:VirtualizationDemo.ViewModels"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             x:Class="VirtualizationDemo.Views.ChatPageView"
+             x:DataType="vm:ChatPageViewModel">
+  <ListBox ItemsSource="{Binding Messages}">
+    <ListBox.ItemContainerTheme>
+      <ControlTheme TargetType="ListBoxItem" BasedOn="{StaticResource {x:Type ListBoxItem}}">
+        <Setter Property="Padding" Value="8"/>
+      </ControlTheme>
+    </ListBox.ItemContainerTheme>
+    <ListBox.ItemTemplate>
+      <DataTemplate>
+        <Border CornerRadius="8" 
+                Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
+                TextElement.Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}"
+                Padding="6"
+                HorizontalAlignment="Left"
+                MaxWidth="280">
+          <DockPanel>
+            <TextBlock DockPanel.Dock="Top"
+                       Text="{Binding Sender}"
+                       FontWeight="Bold"/>
+            <TextBlock DockPanel.Dock="Bottom" 
+                       Text="{Binding Timestamp}"
+                       FontSize="10"
+                       Foreground="{DynamicResource SystemControlForegroundBaseMediumBrush}"
+                       TextAlignment="Right"
+                       Margin="0 4 0 0"/>
+            <TextBlock Text="{Binding Message}" TextWrapping="Wrap"/>
+          </DockPanel>
+        </Border>
+      </DataTemplate>
+    </ListBox.ItemTemplate>
+  </ListBox>
+</UserControl>

+ 11 - 0
samples/VirtualizationDemo/Views/ChatPageView.axaml.cs

@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace VirtualizationDemo.Views;
+
+public partial class ChatPageView : UserControl
+{
+    public ChatPageView()
+    {
+        InitializeComponent();
+    }
+}

+ 13 - 8
samples/VirtualizationDemo/VirtualizationDemo.csproj

@@ -1,19 +1,24 @@
 <Project Sdk="Microsoft.NET.Sdk">
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
   <PropertyGroup>
-    <OutputType>Exe</OutputType>
+    <OutputType>WinExe</OutputType>
     <TargetFramework>net6.0</TargetFramework>
     <TargetFramework>net6.0</TargetFramework>
+    <IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators>
   </PropertyGroup>
   </PropertyGroup>
   <ItemGroup>
   <ItemGroup>
+    <ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Desktop\Avalonia.Desktop.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
-    <ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
-    <ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
     <ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />
     <ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />
+    <ProjectReference Include="..\SampleControls\ControlSamples.csproj" />
+  </ItemGroup>
+ <ItemGroup>
+    <None Update="Assets\chat.json">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
   </ItemGroup>
   </ItemGroup>
-  <Import Project="..\..\build\SampleApp.props" />
-  <Import Project="..\..\build\EmbedXaml.props" />
-  <Import Project="..\..\build\Rx.props" />
-  <Import Condition="'$(TargetFramework)'=='net461'" Project="..\..\build\NetFX.props" />
-  <Import Project="..\..\build\ReferenceCoreLibraries.props" />
   <Import Project="..\..\build\BuildTargets.targets" />
   <Import Project="..\..\build\BuildTargets.targets" />
+  <Import Project="..\..\build\SourceGenerators.props" />
+  <Import Project="..\..\build\NullableEnable.props" />
 </Project>
 </Project>