diff --git a/Avalonia.sln b/Avalonia.sln
index 3c2e8fc83d..21c317549c 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
-VisualStudioVersion = 14.0.25123.0
+VisualStudioVersion = 14.0.24720.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Base", "src\Avalonia.Base\Avalonia.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}"
EndProject
@@ -15,6 +15,9 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Direct2D1", "src\Windows\Avalonia.Direct2D1\Avalonia.Direct2D1.csproj", "{3E908F67-5543-4879-A1DC-08EACE79B3CD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Designer", "src\Windows\Avalonia.Designer\Avalonia.Designer.csproj", "{EC42600F-049B-43FF-AED1-8314D61B2749}"
+ ProjectSection(ProjectDependencies) = postProject
+ {2B888490-D14A-4BCA-AB4B-48676FA93C9B} = {2B888490-D14A-4BCA-AB4B-48676FA93C9B}
+ EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Input", "src\Avalonia.Input\Avalonia.Input.csproj", "{62024B2D-53EB-4638-B26B-85EEAA54866E}"
EndProject
@@ -158,6 +161,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.DesignerSupport.Te
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Avalonia.RenderTests", "tests\Avalonia.RenderTests\Avalonia.RenderTests.shproj", "{48840EDD-24BF-495D-911E-2EB12AE75D3B}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VirtualizationTest", "samples\VirtualizationTest\VirtualizationTest.csproj", "{FBCAF3D0-2808-4934-8E96-3F607594517B}"
+EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Shared\RenderHelpers\RenderHelpers.projitems*{fb05ac90-89ba-4f2f-a924-f37875fb547c}*SharedItemsImports = 4
@@ -1909,6 +1914,38 @@ Global
{F1381F98-4D24-409A-A6C5-1C5B1E08BB08}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{F1381F98-4D24-409A-A6C5-1C5B1E08BB08}.Release|x86.ActiveCfg = Release|Any CPU
{F1381F98-4D24-409A-A6C5-1C5B1E08BB08}.Release|x86.Build.0 = Release|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|x86.ActiveCfg = Release|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|x86.Build.0 = Release|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|Any CPU.ActiveCfg = Release|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|Any CPU.Build.0 = Release|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|iPhone.ActiveCfg = Release|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|iPhone.Build.0 = Release|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|x86.ActiveCfg = Release|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|x86.Build.0 = Release|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|x86.Build.0 = Debug|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|iPhone.Build.0 = Release|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|x86.ActiveCfg = Release|Any CPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1960,5 +1997,6 @@ Global
{52F55355-D120-42AC-8116-8410A7D602FA} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{F1381F98-4D24-409A-A6C5-1C5B1E08BB08} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{48840EDD-24BF-495D-911E-2EB12AE75D3B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
+ {FBCAF3D0-2808-4934-8E96-3F607594517B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
EndGlobalSection
EndGlobal
diff --git a/nuget/template/Avalonia.Skia.Desktop.nuspec b/nuget/template/Avalonia.Skia.Desktop.nuspec
index aa52ac4fa1..ab962c5521 100644
--- a/nuget/template/Avalonia.Skia.Desktop.nuspec
+++ b/nuget/template/Avalonia.Skia.Desktop.nuspec
@@ -13,7 +13,7 @@
Copyright 2015
Avalonia
-
+
diff --git a/readme.md b/readme.md
index 1c77a38c08..63047fdc85 100644
--- a/readme.md
+++ b/readme.md
@@ -45,7 +45,7 @@ framework.
As mentioned above, Avalonia is still in alpha and as such there's not much documentation yet. You can
take a look at the [getting started page](docs/tutorial/gettingstarted.md) for an
overview of how to get started but probably the best thing to do for now is to already know a little bit
-about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/Avalonia/Avalonia).
+about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/AvaloniaUI/Avalonia).
There's also a high-level [architecture document](docs/spec/architecture.md) that is currently a little bit
out of date, and I've also started writing blog posts on Avalonia at http://grokys.github.io/.
diff --git a/samples/BindingTest/App.xaml.cs b/samples/BindingTest/App.xaml.cs
index fd457f7ba9..7bcaf837a8 100644
--- a/samples/BindingTest/App.xaml.cs
+++ b/samples/BindingTest/App.xaml.cs
@@ -19,8 +19,7 @@ namespace BindingTest
InitializeLogging();
AppBuilder.Configure()
- .UseWin32()
- .UseDirect2D1()
+ .UsePlatformDetect()
.Start();
}
diff --git a/samples/ControlCatalog.Desktop/Program.cs b/samples/ControlCatalog.Desktop/Program.cs
index ae8c9f5d8f..03842f78c1 100644
--- a/samples/ControlCatalog.Desktop/Program.cs
+++ b/samples/ControlCatalog.Desktop/Program.cs
@@ -17,8 +17,7 @@ namespace ControlCatalog
// TODO: Make this work with GTK/Skia/Cairo depending on command-line args
// again.
AppBuilder.Configure()
- .UseWin32()
- .UseDirect2D1()
+ .UsePlatformDetect()
.Start();
}
diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml b/samples/ControlCatalog/Pages/MenuPage.xaml
index ca28fadea0..98171f29d6 100644
--- a/samples/ControlCatalog/Pages/MenuPage.xaml
+++ b/samples/ControlCatalog/Pages/MenuPage.xaml
@@ -10,6 +10,7 @@
+
diff --git a/samples/TestApplication/Program.cs b/samples/TestApplication/Program.cs
index 5e79916a8d..9e331c8634 100644
--- a/samples/TestApplication/Program.cs
+++ b/samples/TestApplication/Program.cs
@@ -35,8 +35,7 @@ namespace TestApplication
var app = new App();
AppBuilder.Configure(app)
- .UseWin32()
- .UseDirect2D1()
+ .UsePlatformDetect()
.SetupWithoutStarting();
app.Run();
diff --git a/samples/VirtualizationTest/App.config b/samples/VirtualizationTest/App.config
new file mode 100644
index 0000000000..88fa4027bd
--- /dev/null
+++ b/samples/VirtualizationTest/App.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/VirtualizationTest/App.xaml b/samples/VirtualizationTest/App.xaml
new file mode 100644
index 0000000000..d9630eef58
--- /dev/null
+++ b/samples/VirtualizationTest/App.xaml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/VirtualizationTest/App.xaml.cs b/samples/VirtualizationTest/App.xaml.cs
new file mode 100644
index 0000000000..14ab5b3f84
--- /dev/null
+++ b/samples/VirtualizationTest/App.xaml.cs
@@ -0,0 +1,16 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Avalonia;
+using Avalonia.Markup.Xaml;
+
+namespace VirtualizationTest
+{
+ public class App : Application
+ {
+ public override void Initialize()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/samples/VirtualizationTest/MainWindow.xaml b/samples/VirtualizationTest/MainWindow.xaml
new file mode 100644
index 0000000000..55bd729fec
--- /dev/null
+++ b/samples/VirtualizationTest/MainWindow.xaml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+ Add Item
+ Remove Item
+ Recreate
+ Select First
+ Select Last
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/VirtualizationTest/MainWindow.xaml.cs b/samples/VirtualizationTest/MainWindow.xaml.cs
new file mode 100644
index 0000000000..952383dffb
--- /dev/null
+++ b/samples/VirtualizationTest/MainWindow.xaml.cs
@@ -0,0 +1,25 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using VirtualizationTest.ViewModels;
+
+namespace VirtualizationTest
+{
+ public class MainWindow : Window
+ {
+ public MainWindow()
+ {
+ this.InitializeComponent();
+ this.AttachDevTools();
+ DataContext = new MainWindowViewModel();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/samples/VirtualizationTest/Program.cs b/samples/VirtualizationTest/Program.cs
new file mode 100644
index 0000000000..8958605a3b
--- /dev/null
+++ b/samples/VirtualizationTest/Program.cs
@@ -0,0 +1,34 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Logging.Serilog;
+using Serilog;
+
+namespace VirtualizationTest
+{
+ class Program
+ {
+ static void Main(string[] args)
+ {
+ InitializeLogging();
+
+ AppBuilder.Configure()
+ .UseWin32()
+ .UseDirect2D1()
+ .Start();
+ }
+
+ private static void InitializeLogging()
+ {
+#if DEBUG
+ SerilogLogger.Initialize(new LoggerConfiguration()
+ .MinimumLevel.Warning()
+ .WriteTo.Trace(outputTemplate: "{Area}: {Message}")
+ .CreateLogger());
+#endif
+ }
+ }
+}
diff --git a/samples/VirtualizationTest/Properties/AssemblyInfo.cs b/samples/VirtualizationTest/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..e6af64555b
--- /dev/null
+++ b/samples/VirtualizationTest/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("VirtualizationTest")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("VirtualizationTest")]
+[assembly: AssemblyCopyright("Copyright © 2016")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("fbcaf3d0-2808-4934-8e96-3f607594517b")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/samples/VirtualizationTest/ViewModels/ItemViewModel.cs b/samples/VirtualizationTest/ViewModels/ItemViewModel.cs
new file mode 100644
index 0000000000..75777012c1
--- /dev/null
+++ b/samples/VirtualizationTest/ViewModels/ItemViewModel.cs
@@ -0,0 +1,22 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using ReactiveUI;
+
+namespace VirtualizationTest.ViewModels
+{
+ internal class ItemViewModel : ReactiveObject
+ {
+ private string _prefix;
+ private int _index;
+
+ public ItemViewModel(int index, string prefix = "Item")
+ {
+ _prefix = prefix;
+ _index = index;
+ }
+
+ public string Header => $"{_prefix} {_index}";
+ }
+}
diff --git a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs
new file mode 100644
index 0000000000..86869eb46a
--- /dev/null
+++ b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs
@@ -0,0 +1,141 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Collections;
+using Avalonia.Controls;
+using ReactiveUI;
+
+namespace VirtualizationTest.ViewModels
+{
+ internal class MainWindowViewModel : ReactiveObject
+ {
+ private int _itemCount = 200;
+ private string _newItemString = "New Item";
+ private int _newItemIndex;
+ private IReactiveList _items;
+ private string _prefix = "Item";
+ private Orientation _orientation;
+ private ItemVirtualizationMode _virtualizationMode = ItemVirtualizationMode.Simple;
+
+ public MainWindowViewModel()
+ {
+ this.WhenAnyValue(x => x.ItemCount).Subscribe(ResizeItems);
+ RecreateCommand = ReactiveCommand.Create();
+ RecreateCommand.Subscribe(_ => Recreate());
+
+ AddItemCommand = ReactiveCommand.Create();
+ AddItemCommand.Subscribe(_ => AddItem());
+
+ RemoveItemCommand = ReactiveCommand.Create();
+ RemoveItemCommand.Subscribe(_ => Remove());
+
+ SelectFirstCommand = ReactiveCommand.Create();
+ SelectFirstCommand.Subscribe(_ => SelectItem(0));
+
+ SelectLastCommand = ReactiveCommand.Create();
+ SelectLastCommand.Subscribe(_ => 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 AvaloniaList SelectedItems { get; }
+ = new AvaloniaList();
+
+ public IReactiveList 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 Orientations =>
+ Enum.GetValues(typeof(Orientation)).Cast();
+
+ public ItemVirtualizationMode VirtualizationMode
+ {
+ get { return _virtualizationMode; }
+ set { this.RaiseAndSetIfChanged(ref _virtualizationMode, value); }
+ }
+
+ public IEnumerable VirtualizationModes =>
+ Enum.GetValues(typeof(ItemVirtualizationMode)).Cast();
+
+ public ReactiveCommand AddItemCommand { get; private set; }
+ public ReactiveCommand RecreateCommand { get; private set; }
+ public ReactiveCommand RemoveItemCommand { get; private set; }
+ public ReactiveCommand SelectFirstCommand { get; private set; }
+ public ReactiveCommand SelectLastCommand { get; private set; }
+
+ private void ResizeItems(int count)
+ {
+ if (Items == null)
+ {
+ var items = Enumerable.Range(0, count)
+ .Select(x => new ItemViewModel(x));
+ Items = new ReactiveList(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 (SelectedItems.Count > 0)
+ {
+ index = Items.IndexOf(SelectedItems[0]);
+ }
+
+ Items.Insert(index, new ItemViewModel(_newItemIndex++, NewItemString));
+ }
+
+ private void Remove()
+ {
+ if (SelectedItems.Count > 0)
+ {
+ Items.RemoveAll(SelectedItems);
+ }
+ }
+
+ private void Recreate()
+ {
+ _prefix = _prefix == "Item" ? "Recreated" : "Item";
+ var items = Enumerable.Range(0, _itemCount)
+ .Select(x => new ItemViewModel(x, _prefix));
+ Items = new ReactiveList(items);
+ }
+
+ private void SelectItem(int index)
+ {
+ SelectedItems.Clear();
+ SelectedItems.Add(Items[index]);
+ }
+ }
+}
diff --git a/samples/VirtualizationTest/VirtualizationTest.csproj b/samples/VirtualizationTest/VirtualizationTest.csproj
new file mode 100644
index 0000000000..ed7a340234
--- /dev/null
+++ b/samples/VirtualizationTest/VirtualizationTest.csproj
@@ -0,0 +1,180 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {FBCAF3D0-2808-4934-8E96-3F607594517B}
+ WinExe
+ Properties
+ VirtualizationTest
+ VirtualizationTest
+ v4.5.2
+ 512
+ true
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+
+
+
+ ..\..\packages\Serilog.1.5.14\lib\net45\Serilog.dll
+ True
+
+
+ ..\..\packages\Serilog.1.5.14\lib\net45\Serilog.FullNetFx.dll
+ True
+
+
+ ..\..\packages\Splat.1.6.2\lib\Net45\Splat.dll
+ True
+
+
+
+
+ ..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll
+ True
+
+
+ ..\..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll
+ True
+
+
+ ..\..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll
+ True
+
+
+ ..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll
+ True
+
+
+
+
+
+
+
+
+
+
+ App.xaml
+
+
+ MainWindow.xaml
+
+
+
+
+
+
+
+
+
+
+
+
+ {d211e587-d8bc-45b9-95a4-f297c8fa5200}
+ Avalonia.Animation
+
+
+ {b09b78d8-9b26-48b0-9149-d64a2f120f3f}
+ Avalonia.Base
+
+
+ {d2221c82-4a25-4583-9b43-d791e3f6820c}
+ Avalonia.Controls
+
+
+ {799a7bb5-3c2c-48b6-85a7-406a12c420da}
+ Avalonia.DesignerSupport
+
+
+ {7062ae20-5dcc-4442-9645-8195bdece63e}
+ Avalonia.Diagnostics
+
+
+ {62024b2d-53eb-4638-b26b-85eeaa54866e}
+ Avalonia.Input
+
+
+ {6b0ed19d-a08b-461c-a9d9-a9ee40b0c06b}
+ Avalonia.Interactivity
+
+
+ {42472427-4774-4c81-8aff-9f27b8e31721}
+ Avalonia.Layout
+
+
+ {B61B66A3-B82D-4875-8001-89D3394FE0C9}
+ Avalonia.Logging.Serilog
+
+
+ {6417b24e-49c2-4985-8db2-3ab9d898ec91}
+ Avalonia.ReactiveUI
+
+
+ {eb582467-6abb-43a1-b052-e981ba910e3a}
+ Avalonia.SceneGraph
+
+
+ {f1baa01a-f176-4c6a-b39d-5b40bb1b148f}
+ Avalonia.Styling
+
+
+ {3e10a5fa-e8da-48b1-ad44-6a5b6cb7750f}
+ Avalonia.Themes.Default
+
+
+ {3e53a01a-b331-47f3-b828-4a5717e77a24}
+ Avalonia.Markup.Xaml
+
+
+ {6417e941-21bc-467b-a771-0de389353ce6}
+ Avalonia.Markup
+
+
+ {3e908f67-5543-4879-a1dc-08eace79b3cd}
+ Avalonia.Direct2D1
+
+
+ {811a76cf-1cf6-440f-963b-bbe31bd72a82}
+ Avalonia.Win32
+
+
+
+
+ Designer
+
+
+
+
+ Designer
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/VirtualizationTest/VirtualizationTest.v2.ncrunchproject b/samples/VirtualizationTest/VirtualizationTest.v2.ncrunchproject
new file mode 100644
index 0000000000..f744eecae0
--- /dev/null
+++ b/samples/VirtualizationTest/VirtualizationTest.v2.ncrunchproject
@@ -0,0 +1,26 @@
+
+ true
+ 1000
+ false
+ false
+ false
+ true
+ false
+ false
+ true
+ false
+ false
+ true
+ true
+ false
+ true
+ true
+ true
+ 60000
+
+
+
+ AutoDetect
+ STA
+ x86
+
\ No newline at end of file
diff --git a/samples/VirtualizationTest/packages.config b/samples/VirtualizationTest/packages.config
new file mode 100644
index 0000000000..782e7fbf2c
--- /dev/null
+++ b/samples/VirtualizationTest/packages.config
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/XamlTestApplication/Program.cs b/samples/XamlTestApplication/Program.cs
index 50b3c364d4..6485796ce7 100644
--- a/samples/XamlTestApplication/Program.cs
+++ b/samples/XamlTestApplication/Program.cs
@@ -21,8 +21,7 @@ namespace XamlTestApplication
InitializeLogging();
AppBuilder.Configure()
- .UseWin32()
- .UseDirect2D1()
+ .UsePlatformDetect()
.Start();
}
diff --git a/samples/XamlTestApplicationPcl/TestScrollable.cs b/samples/XamlTestApplicationPcl/TestScrollable.cs
index 9ec70a4eb1..9d4c1d9b29 100644
--- a/samples/XamlTestApplicationPcl/TestScrollable.cs
+++ b/samples/XamlTestApplicationPcl/TestScrollable.cs
@@ -2,11 +2,13 @@ using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
+using Avalonia.Input;
using Avalonia.Media;
+using Avalonia.VisualTree;
namespace XamlTestApplication
{
- public class TestScrollable : Control, IScrollable
+ public class TestScrollable : Control, ILogicalScrollable
{
private int itemCount = 100;
private Size _extent;
@@ -14,6 +16,7 @@ namespace XamlTestApplication
private Size _viewport;
private Size _lineSize;
+ public bool IsLogicalScrollEnabled => true;
public Action InvalidateScroll { get; set; }
Size IScrollable.Extent
@@ -53,6 +56,36 @@ namespace XamlTestApplication
}
}
+ public override void Render(DrawingContext context)
+ {
+ var y = 0.0;
+
+ for (var i = (int)_offset.Y; i < itemCount; ++i)
+ {
+ using (var line = new FormattedText(
+ "Item " + (i + 1),
+ TextBlock.GetFontFamily(this),
+ TextBlock.GetFontSize(this),
+ TextBlock.GetFontStyle(this),
+ TextAlignment.Left,
+ TextBlock.GetFontWeight(this)))
+ {
+ context.DrawText(Brushes.Black, new Point(-_offset.X, y), line);
+ y += _lineSize.Height;
+ }
+ }
+ }
+
+ public bool BringIntoView(IControl target, Rect targetRect)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IControl GetControlInDirection(NavigationDirection direction, IControl from)
+ {
+ throw new NotImplementedException();
+ }
+
protected override Size MeasureOverride(Size availableSize)
{
using (var line = new FormattedText(
@@ -76,25 +109,5 @@ namespace XamlTestApplication
InvalidateScroll?.Invoke();
return finalSize;
}
-
- public override void Render(DrawingContext context)
- {
- var y = 0.0;
-
- for (var i = (int)_offset.Y; i < itemCount; ++i)
- {
- using (var line = new FormattedText(
- "Item " + (i + 1),
- TextBlock.GetFontFamily(this),
- TextBlock.GetFontSize(this),
- TextBlock.GetFontStyle(this),
- TextAlignment.Left,
- TextBlock.GetFontWeight(this)))
- {
- context.DrawText(Brushes.Black, new Point(-_offset.X, y), line);
- y += _lineSize.Height;
- }
- }
- }
}
}
\ No newline at end of file
diff --git a/samples/XamlTestApplicationPcl/Views/MainWindow.cs b/samples/XamlTestApplicationPcl/Views/MainWindow.cs
index 2c0012d65f..8a19cef15d 100644
--- a/samples/XamlTestApplicationPcl/Views/MainWindow.cs
+++ b/samples/XamlTestApplicationPcl/Views/MainWindow.cs
@@ -1,6 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using System.Collections.Generic;
+using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Diagnostics;
diff --git a/samples/XamlTestApplicationPcl/Views/MainWindow.xaml b/samples/XamlTestApplicationPcl/Views/MainWindow.xaml
index b93072d2d5..a844986169 100644
--- a/samples/XamlTestApplicationPcl/Views/MainWindow.xaml
+++ b/samples/XamlTestApplicationPcl/Views/MainWindow.xaml
@@ -24,6 +24,7 @@
+
diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs
index 012e19da50..3192629494 100644
--- a/src/Android/Avalonia.Android/AndroidPlatform.cs
+++ b/src/Android/Avalonia.Android/AndroidPlatform.cs
@@ -38,7 +38,6 @@ namespace Avalonia.Android
.Bind().ToConstant(this);
SkiaPlatform.Initialize();
- Application.RegisterPlatformCallback(() => { });
_scalingFactor = global::Android.App.Application.Context.Resources.DisplayMetrics.ScaledDensity;
diff --git a/src/Android/Avalonia.Android/Avalonia.Android.v2.ncrunchproject b/src/Android/Avalonia.Android/Avalonia.Android.v2.ncrunchproject
index 30815b1937..f744eecae0 100644
--- a/src/Android/Avalonia.Android/Avalonia.Android.v2.ncrunchproject
+++ b/src/Android/Avalonia.Android/Avalonia.Android.v2.ncrunchproject
@@ -7,7 +7,7 @@
true
false
false
- false
+ true
false
false
true
@@ -17,9 +17,9 @@
true
true
60000
-
-
-
+
+
+
AutoDetect
STA
x86
diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs
index dd047db279..b0db89f7ea 100644
--- a/src/Avalonia.Base/AvaloniaObject.cs
+++ b/src/Avalonia.Base/AvaloniaObject.cs
@@ -26,7 +26,7 @@ namespace Avalonia
///
/// The parent object that inherited values are inherited from.
///
- private AvaloniaObject _inheritanceParent;
+ private IAvaloniaObject _inheritanceParent;
///
/// The set values/bindings on this object.
@@ -120,7 +120,7 @@ namespace Avalonia
///
/// The inheritance parent.
///
- protected AvaloniaObject InheritanceParent
+ protected IAvaloniaObject InheritanceParent
{
get
{
diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs
index e893fcb75b..65b559482d 100644
--- a/src/Avalonia.Base/Collections/AvaloniaList.cs
+++ b/src/Avalonia.Base/Collections/AvaloniaList.cs
@@ -319,6 +319,57 @@ namespace Avalonia.Collections
}
}
+ ///
+ /// Moves an item to a new index.
+ ///
+ /// The index of the item to move.
+ /// The index to move the item to.
+ public void Move(int oldIndex, int newIndex)
+ {
+ var item = this[oldIndex];
+ _inner.RemoveAt(oldIndex);
+ _inner.Insert(newIndex, item);
+
+ if (_collectionChanged != null)
+ {
+ var e = new NotifyCollectionChangedEventArgs(
+ NotifyCollectionChangedAction.Move,
+ item,
+ newIndex,
+ oldIndex);
+ _collectionChanged(this, e);
+ }
+ }
+
+ ///
+ /// Moves multiple items to a new index.
+ ///
+ /// The first index of the items to move.
+ /// The number of items to move.
+ /// The index to move the items to.
+ public void MoveRange(int oldIndex, int count, int newIndex)
+ {
+ var items = _inner.GetRange(oldIndex, count);
+ _inner.RemoveRange(oldIndex, count);
+
+ if (newIndex > oldIndex)
+ {
+ newIndex -= count;
+ }
+
+ _inner.InsertRange(newIndex, items);
+
+ if (_collectionChanged != null)
+ {
+ var e = new NotifyCollectionChangedEventArgs(
+ NotifyCollectionChangedAction.Move,
+ items,
+ newIndex,
+ oldIndex);
+ _collectionChanged(this, e);
+ }
+ }
+
///
/// Removes an item from the collection.
///
diff --git a/src/Avalonia.Controls/AppBuilder.cs b/src/Avalonia.Controls/AppBuilder.cs
index 86ba8c3786..8b3e0e731a 100644
--- a/src/Avalonia.Controls/AppBuilder.cs
+++ b/src/Avalonia.Controls/AppBuilder.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
+using System.Reflection;
namespace Avalonia.Controls
{
@@ -98,23 +99,64 @@ namespace Avalonia.Controls
///
/// The method to call to initialize the windowing subsystem.
/// An instance.
- public AppBuilder WithWindowingSubsystem(Action initializer)
+ public AppBuilder UseWindowingSubsystem(Action initializer)
{
WindowingSubsystem = initializer;
return this;
}
+ ///
+ /// Specifies a windowing subsystem to use.
+ ///
+ /// The dll in which to look for subsystem.
+ /// An instance.
+ public AppBuilder UseWindowingSubsystem(string dll) => UseWindowingSubsystem(GetInitializer(dll));
+
///
/// Specifies a rendering subsystem to use.
///
/// The method to call to initialize the rendering subsystem.
/// An instance.
- public AppBuilder WithRenderingSubsystem(Action initializer)
+ public AppBuilder UseRenderingSubsystem(Action initializer)
{
RenderingSubsystem = initializer;
return this;
}
+ ///
+ /// Specifies a rendering subsystem to use.
+ ///
+ /// The dll in which to look for subsystem.
+ /// An instance.
+ public AppBuilder UseRenderingSubsystem(string dll) => UseRenderingSubsystem(GetInitializer(dll));
+
+ static Action GetInitializer(string assemblyName) => () =>
+ {
+ var assembly = Assembly.Load(new AssemblyName(assemblyName));
+ var platformClassName = assemblyName.Replace("Avalonia.", string.Empty) + "Platform";
+ var platformClassFullName = assemblyName + "." + platformClassName;
+ var platformClass = assembly.GetType(platformClassFullName);
+ var init = platformClass.GetRuntimeMethod("Initialize", new Type[0]);
+ init.Invoke(null, null);
+ };
+
+ public AppBuilder UsePlatformDetect()
+ {
+ var platformId = (int)
+ ((dynamic) Type.GetType("System.Environment").GetRuntimeProperty("OSVersion").GetValue(null)).Platform;
+ if (platformId == 4 || platformId == 6)
+ {
+ UseRenderingSubsystem("Avalonia.Cairo");
+ UseWindowingSubsystem("Avalonia.Gtk");
+ }
+ else
+ {
+ UseRenderingSubsystem("Avalonia.Direct2D1");
+ UseWindowingSubsystem("Avalonia.Win32");
+ }
+ return this;
+ }
+
///
/// Sets up the platform-speciic services for the .
///
diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs
index f73ead5576..1991f49ac0 100644
--- a/src/Avalonia.Controls/Application.cs
+++ b/src/Avalonia.Controls/Application.cs
@@ -26,16 +26,12 @@ namespace Avalonia
/// - A global set of .
/// - A .
/// - An .
- /// - Loads and initializes rendering and windowing subsystems with
- /// and .
/// - Registers services needed by the rest of Avalonia in the
/// method.
/// - Tracks the lifetime of the application.
///
public class Application : IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IApplicationLifecycle
{
- static Action _platformInitializationCallback;
-
///
/// The application-global data templates.
///
@@ -121,11 +117,6 @@ namespace Avalonia
///
IStyleHost IStyleHost.StylingParent => null;
- public static void RegisterPlatformCallback(Action cb)
- {
- _platformInitializationCallback = cb;
- }
-
///
/// Initializes the application by loading XAML etc.
///
@@ -189,47 +180,5 @@ namespace Avalonia
.Bind().ToTransient()
.Bind().ToConstant(this);
}
-
- ///
- /// Initializes the rendering and windowing subsystems according to platform.
- ///
- /// The value of Environment.OSVersion.Platform.
- protected void InitializeSubsystems(int platformID)
- {
- if (_platformInitializationCallback != null)
- {
- _platformInitializationCallback();
- }
- else if (platformID == 4 || platformID == 6)
- {
- InitializeSubsystem("Avalonia.Cairo");
- InitializeSubsystem("Avalonia.Gtk");
- }
- else
- {
- InitializeSubsystem("Avalonia.Direct2D1");
- InitializeSubsystem("Avalonia.Win32");
- }
- }
-
- ///
- /// Initializes the rendering or windowing subsystem defined by the specified assemblt.
- ///
- /// The name of the assembly.
- protected static void InitializeSubsystem(string assemblyName)
- {
- var assembly = Assembly.Load(new AssemblyName(assemblyName));
- var platformClassName = assemblyName.Replace("Avalonia.", string.Empty) + "Platform";
- var platformClassFullName = assemblyName + "." + platformClassName;
- var platformClass = assembly.GetType(platformClassFullName);
- var init = platformClass.GetRuntimeMethod("Initialize", new Type[0]);
- init.Invoke(null, null);
- }
-
- internal static void InitializeWin32Subsystem()
- {
- InitializeSubsystem("Avalonia.Direct2D1");
- InitializeSubsystem("Avalonia.Win32");
- }
}
}
diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj
index 6358a8c9d9..3f8985aa12 100644
--- a/src/Avalonia.Controls/Avalonia.Controls.csproj
+++ b/src/Avalonia.Controls/Avalonia.Controls.csproj
@@ -50,26 +50,31 @@
-
+
+
-
+
+
+
+
+
-
-
-
+
+
+
-
+
@@ -170,6 +175,7 @@
+
diff --git a/src/Avalonia.Controls/Canvas.cs b/src/Avalonia.Controls/Canvas.cs
index ebccb7435e..74eebea4bc 100644
--- a/src/Avalonia.Controls/Canvas.cs
+++ b/src/Avalonia.Controls/Canvas.cs
@@ -136,7 +136,7 @@ namespace Avalonia.Controls
/// The movement direction.
/// The control from which movement begins.
/// The control.
- IInputElement INavigableContainer.GetControl(FocusNavigationDirection direction, IInputElement from)
+ IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from)
{
// TODO: Implement this
return null;
diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs
index e246635906..944f6b82ac 100644
--- a/src/Avalonia.Controls/Control.cs
+++ b/src/Avalonia.Controls/Control.cs
@@ -33,7 +33,7 @@ namespace Avalonia.Controls
/// - Implements to allow styling to work on the control.
/// - Implements to form part of a logical tree.
///
- public class Control : InputElement, IControl, INamed, ISetLogicalParent, ISupportInitialize
+ public class Control : InputElement, IControl, INamed, ISetInheritanceParent, ISetLogicalParent, ISupportInitialize
{
///
/// Defines the property.
@@ -435,7 +435,11 @@ namespace Avalonia.Controls
OnDetachedFromLogicalTreeCore(e);
}
- InheritanceParent = parent as AvaloniaObject;
+ if (InheritanceParent == null || parent == null)
+ {
+ InheritanceParent = parent as AvaloniaObject;
+ }
+
_parent = (IControl)parent;
if (_parent is IStyleRoot || _parent?.IsAttachedToLogicalTree == true)
@@ -455,6 +459,15 @@ namespace Avalonia.Controls
}
}
+ ///
+ /// Sets the control's inheritance parent.
+ ///
+ /// The parent.
+ void ISetInheritanceParent.SetParent(IAvaloniaObject parent)
+ {
+ InheritanceParent = parent;
+ }
+
///
/// Adds a pseudo-class to be set when a property is true.
///
diff --git a/src/Avalonia.Controls/Design.cs b/src/Avalonia.Controls/Design.cs
index 231914740e..a0b0d5d0ca 100644
--- a/src/Avalonia.Controls/Design.cs
+++ b/src/Avalonia.Controls/Design.cs
@@ -1,4 +1,6 @@
+using System.Runtime.CompilerServices;
+
namespace Avalonia.Controls
{
public static class Design
@@ -43,7 +45,25 @@ namespace Avalonia.Controls
{
return control.GetValue(DataContextProperty);
}
-
+
+ static readonly ConditionalWeakTable Substitutes = new ConditionalWeakTable();
+
+ public static readonly AttachedProperty PreviewWithProperty = AvaloniaProperty
+ .RegisterAttached("PreviewWith", typeof (Design));
+
+ public static void SetPreviewWith(object target, Control control)
+ {
+ Substitutes.Remove(target);
+ Substitutes.Add(target, control);
+ }
+
+ public static Control GetPreviewWith(object target)
+ {
+ Control rv;
+ Substitutes.TryGetValue(target, out rv);
+ return rv;
+ }
+
internal static void ApplyDesignerProperties(Control target, Control source)
{
if (source.IsSet(WidthProperty))
diff --git a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs
index 8199333f47..ba584e33b9 100644
--- a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs
+++ b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
-using System.Collections;
using System.Collections.Generic;
using Avalonia.Controls.Templates;
@@ -16,7 +15,7 @@ namespace Avalonia.Controls.Generators
///
/// Gets the currently realized containers.
///
- IEnumerable Containers { get; }
+ IEnumerable Containers { get; }
///
/// Gets or sets the data template used to display the items in the control.
@@ -34,28 +33,33 @@ namespace Avalonia.Controls.Generators
event EventHandler Dematerialized;
///
- /// Creates container controls for a collection of items.
+ /// Event raised whenever containers are recycled.
///
- ///
- /// The index of the first item of the data in the containing collection.
+ event EventHandler Recycled;
+
+ ///
+ /// Creates a container control for an item.
+ ///
+ ///
+ /// The index of the item of data in the control's items.
///
- /// The items.
+ /// The item.
/// An optional member selector.
/// The created controls.
- IEnumerable Materialize(
- int startingIndex,
- IEnumerable items,
+ ItemContainerInfo Materialize(
+ int index,
+ object item,
IMemberSelector selector);
///
/// Removes a set of created containers.
///
///
- /// The index of the first item of the data in the containing collection.
+ /// The index of the first item in the control's items.
///
/// The the number of items to remove.
/// The removed containers.
- IEnumerable Dematerialize(int startingIndex, int count);
+ IEnumerable Dematerialize(int startingIndex, int count);
///
/// Inserts space for newly inserted containers in the index.
@@ -69,17 +73,23 @@ namespace Avalonia.Controls.Generators
/// the gap.
///
///
- /// The index of the first item of the data in the containing collection.
+ /// The index of the first item in the control's items.
///
/// The the number of items to remove.
/// The removed containers.
- IEnumerable RemoveRange(int startingIndex, int count);
+ IEnumerable RemoveRange(int startingIndex, int count);
+
+ bool TryRecycle(
+ int oldIndex,
+ int newIndex,
+ object item,
+ IMemberSelector selector);
///
/// Clears all created containers and returns the removed controls.
///
/// The removed controls.
- IEnumerable Clear();
+ IEnumerable Clear();
///
/// Gets the container control representing the item with the specified index.
diff --git a/src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs b/src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs
index 304e0a99fa..6821a842e3 100644
--- a/src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs
+++ b/src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs
@@ -15,13 +15,10 @@ namespace Avalonia.Controls.Generators
///
/// Initializes a new instance of the class.
///
- /// The index of the first container in the source items.
/// The container.
- public ItemContainerEventArgs(
- int startingIndex,
- ItemContainer container)
+ public ItemContainerEventArgs(ItemContainerInfo container)
{
- StartingIndex = startingIndex;
+ StartingIndex = container.Index;
Containers = new[] { container };
}
@@ -32,7 +29,7 @@ namespace Avalonia.Controls.Generators
/// The containers.
public ItemContainerEventArgs(
int startingIndex,
- IList containers)
+ IList containers)
{
StartingIndex = startingIndex;
Containers = containers;
@@ -41,7 +38,7 @@ namespace Avalonia.Controls.Generators
///
/// Gets the containers.
///
- public IList Containers { get; }
+ public IList Containers { get; }
///
/// Gets the index of the first container in the source items.
diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
index 114d31c5a6..6bbf757106 100644
--- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
+++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
@@ -2,11 +2,11 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
-using System.Collections;
using System.Collections.Generic;
using System.Linq;
-using System.Reactive.Subjects;
+using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
+using Avalonia.Data;
namespace Avalonia.Controls.Generators
{
@@ -15,7 +15,7 @@ namespace Avalonia.Controls.Generators
///
public class ItemContainerGenerator : IItemContainerGenerator
{
- private List _containers = new List();
+ private Dictionary _containers = new Dictionary();
///
/// Initializes a new instance of the class.
@@ -29,7 +29,7 @@ namespace Avalonia.Controls.Generators
}
///
- public IEnumerable Containers => _containers.Where(x => x != null);
+ public IEnumerable Containers => _containers.Values;
///
public event EventHandler Materialized;
@@ -37,6 +37,9 @@ namespace Avalonia.Controls.Generators
///
public event EventHandler Dematerialized;
+ ///
+ public event EventHandler Recycled;
+
///
/// Gets or sets the data template used to display the items in the control.
///
@@ -48,41 +51,29 @@ namespace Avalonia.Controls.Generators
public IControl Owner { get; }
///
- public IEnumerable Materialize(
- int startingIndex,
- IEnumerable items,
+ public ItemContainerInfo Materialize(
+ int index,
+ object item,
IMemberSelector selector)
{
- Contract.Requires(items != null);
+ var i = selector != null ? selector.Select(item) : item;
+ var container = new ItemContainerInfo(CreateContainer(i), item, index);
- int index = startingIndex;
- var result = new List();
+ _containers.Add(container.Index, container);
+ Materialized?.Invoke(this, new ItemContainerEventArgs(container));
- foreach (var item in items)
- {
- var i = selector != null ? selector.Select(item) : item;
- var container = new ItemContainer(CreateContainer(i), item, index++);
- result.Add(container);
- }
-
- AddContainers(result);
- Materialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result));
-
- return result.Where(x => x != null).ToList();
+ return container;
}
///
- public virtual IEnumerable Dematerialize(int startingIndex, int count)
+ public virtual IEnumerable Dematerialize(int startingIndex, int count)
{
- var result = new List();
+ var result = new List();
for (int i = startingIndex; i < startingIndex + count; ++i)
{
- if (i < _containers.Count)
- {
- result.Add(_containers[i]);
- _containers[i] = null;
- }
+ result.Add(_containers[i]);
+ _containers.Remove(i);
}
Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result));
@@ -93,18 +84,47 @@ namespace Avalonia.Controls.Generators
///
public virtual void InsertSpace(int index, int count)
{
- _containers.InsertRange(index, Enumerable.Repeat(null, count));
+ if (count > 0)
+ {
+ var toMove = _containers.Where(x => x.Key >= index).ToList();
+
+ foreach (var i in toMove)
+ {
+ _containers.Remove(i.Key);
+ i.Value.Index += count;
+ _containers[i.Value.Index] = i.Value;
+ }
+ }
}
///
- public virtual IEnumerable RemoveRange(int startingIndex, int count)
+ public virtual IEnumerable RemoveRange(int startingIndex, int count)
{
- List result = new List();
+ var result = new List();
- if (startingIndex < _containers.Count)
+ if (count > 0)
{
- result.AddRange(_containers.GetRange(startingIndex, count));
- _containers.RemoveRange(startingIndex, count);
+ for (var i = startingIndex; i < startingIndex + count; ++i)
+ {
+ ItemContainerInfo found;
+
+ if (_containers.TryGetValue(i, out found))
+ {
+ result.Add(found);
+ }
+
+ _containers.Remove(i);
+ }
+
+ var toMove = _containers.Where(x => x.Key >= startingIndex).ToList();
+
+ foreach (var i in toMove)
+ {
+ _containers.Remove(i.Key);
+ i.Value.Index -= count;
+ _containers.Add(i.Value.Index, i.Value);
+ }
+
Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result));
}
@@ -112,10 +132,20 @@ namespace Avalonia.Controls.Generators
}
///
- public virtual IEnumerable Clear()
+ public virtual bool TryRecycle(
+ int oldIndex,
+ int newIndex,
+ object item,
+ IMemberSelector selector)
+ {
+ return false;
+ }
+
+ ///
+ public virtual IEnumerable Clear()
{
- var result = _containers.Where(x => x != null).ToList();
- _containers = new List();
+ var result = Containers.ToList();
+ _containers.Clear();
if (result.Count > 0)
{
@@ -128,27 +158,20 @@ namespace Avalonia.Controls.Generators
///
public IControl ContainerFromIndex(int index)
{
- if (index < _containers.Count)
- {
- return _containers[index]?.ContainerControl;
- }
-
- return null;
+ ItemContainerInfo result;
+ _containers.TryGetValue(index, out result);
+ return result?.ContainerControl;
}
///
public int IndexFromContainer(IControl container)
{
- var index = 0;
-
foreach (var i in _containers)
{
- if (i?.ContainerControl == container)
+ if (i.Value.ContainerControl == container)
{
- return index;
+ return i.Key;
}
-
- ++index;
}
return -1;
@@ -161,44 +184,40 @@ namespace Avalonia.Controls.Generators
/// The created container control.
protected virtual IControl CreateContainer(object item)
{
- var result = Owner.MaterializeDataTemplate(item, ItemTemplate);
+ var result = item as IControl;
- if (result != null && !(item is IControl))
+ if (result == null)
{
- result.DataContext = item;
+ result = new ContentPresenter();
+ result.SetValue(ContentPresenter.ContentProperty, item, BindingPriority.Style);
+
+ if (ItemTemplate != null)
+ {
+ result.SetValue(
+ ContentPresenter.ContentTemplateProperty,
+ ItemTemplate,
+ BindingPriority.TemplatedParent);
+ }
}
return result;
}
///
- /// Adds a collection of containers to the index.
+ /// Moves a container.
///
- /// The containers.
- protected void AddContainers(IList containers)
+ /// The old index.
+ /// The new index.
+ /// The new item.
+ /// The container info.
+ protected ItemContainerInfo MoveContainer(int oldIndex, int newIndex, object item)
{
- Contract.Requires(containers != null);
-
- foreach (var c in containers)
- {
- while (_containers.Count < c.Index)
- {
- _containers.Add(null);
- }
-
- if (_containers.Count == c.Index)
- {
- _containers.Add(c);
- }
- else if (_containers[c.Index] == null)
- {
- _containers[c.Index] = c;
- }
- else
- {
- throw new InvalidOperationException("Container already created.");
- }
- }
+ var container = _containers[oldIndex];
+ container.Index = newIndex;
+ container.Item = item;
+ _containers.Remove(oldIndex);
+ _containers.Add(newIndex, container);
+ return container;
}
///
@@ -207,9 +226,18 @@ namespace Avalonia.Controls.Generators
/// The first index.
/// The number of elements in the range.
/// The containers.
- protected IEnumerable GetContainerRange(int index, int count)
+ protected IEnumerable GetContainerRange(int index, int count)
+ {
+ return _containers.Where(x => x.Key >= index && x.Key <= index + count).Select(x => x.Value);
+ }
+
+ ///
+ /// Raises the event.
+ ///
+ /// The event args.
+ protected void RaiseRecycled(ItemContainerEventArgs e)
{
- return _containers.GetRange(index, count);
+ Recycled?.Invoke(this, e);
}
}
}
\ No newline at end of file
diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs
index c4bc730e15..3aa2181cd4 100644
--- a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs
+++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs
@@ -5,6 +5,7 @@ using System;
using System.Linq.Expressions;
using System.Reflection;
using Avalonia.Controls.Templates;
+using Avalonia.Data;
namespace Avalonia.Controls.Generators
{
@@ -62,10 +63,10 @@ namespace Avalonia.Controls.Generators
if (ContentTemplateProperty != null)
{
- result.SetValue(ContentTemplateProperty, ItemTemplate);
+ result.SetValue(ContentTemplateProperty, ItemTemplate, BindingPriority.Style);
}
- result.SetValue(ContentProperty, item);
+ result.SetValue(ContentProperty, item, BindingPriority.Style);
if (!(item is IControl))
{
@@ -75,5 +76,34 @@ namespace Avalonia.Controls.Generators
return result;
}
}
+
+ ///
+ public override bool TryRecycle(
+ int oldIndex,
+ int newIndex,
+ object item,
+ IMemberSelector selector)
+ {
+ var container = ContainerFromIndex(oldIndex);
+
+ if (container == null)
+ {
+ throw new IndexOutOfRangeException("Could not recycle container: not materialized.");
+ }
+
+ var i = selector != null ? selector.Select(item) : item;
+
+ container.SetValue(ContentProperty, i);
+
+ if (!(item is IControl))
+ {
+ container.DataContext = i;
+ }
+
+ var info = MoveContainer(oldIndex, newIndex, i);
+ RaiseRecycled(new ItemContainerEventArgs(info));
+
+ return true;
+ }
}
}
diff --git a/src/Avalonia.Controls/Generators/ItemContainer.cs b/src/Avalonia.Controls/Generators/ItemContainerInfo.cs
similarity index 85%
rename from src/Avalonia.Controls/Generators/ItemContainer.cs
rename to src/Avalonia.Controls/Generators/ItemContainerInfo.cs
index ed2e433a28..b9387f1022 100644
--- a/src/Avalonia.Controls/Generators/ItemContainer.cs
+++ b/src/Avalonia.Controls/Generators/ItemContainerInfo.cs
@@ -7,17 +7,17 @@ namespace Avalonia.Controls.Generators
/// Holds information about an item container generated by an
/// .
///
- public class ItemContainer
+ public class ItemContainerInfo
{
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The container control.
/// The item that the container represents.
///
/// The index of the item in the collection.
///
- public ItemContainer(IControl container, object item, int index)
+ public ItemContainerInfo(IControl container, object item, int index)
{
ContainerControl = container;
Item = item;
@@ -35,11 +35,11 @@ namespace Avalonia.Controls.Generators
///
/// Gets the item that the container represents.
///
- public object Item { get; }
+ public object Item { get; internal set; }
///
/// Gets the index of the item in the collection.
///
- public int Index { get; }
+ public int Index { get; internal set; }
}
}
\ No newline at end of file
diff --git a/src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs
new file mode 100644
index 0000000000..c9b3a55aaa
--- /dev/null
+++ b/src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Avalonia.Controls.Generators
+{
+ public class MenuItemContainerGenerator : ItemContainerGenerator
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The owner control.
+ public MenuItemContainerGenerator(IControl owner)
+ : base(owner, MenuItem.HeaderProperty, null)
+ {
+ }
+
+ ///
+ protected override IControl CreateContainer(object item)
+ {
+ var separator = item as Separator;
+ return separator != null ? separator : base.CreateContainer(item);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs
index b7a9bb2dc1..e0c52beb11 100644
--- a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs
+++ b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs
@@ -47,7 +47,7 @@ namespace Avalonia.Controls.Generators
Materialized?.Invoke(
this,
- new ItemContainerEventArgs(0, new ItemContainer(container, item, 0)));
+ new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0)));
}
///
@@ -62,14 +62,14 @@ namespace Avalonia.Controls.Generators
Dematerialized?.Invoke(
this,
- new ItemContainerEventArgs(0, new ItemContainer(container, item, 0)));
+ new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0)));
}
///
/// Removes a set of containers from the index.
///
/// The item containers.
- public void Remove(IEnumerable containers)
+ public void Remove(IEnumerable containers)
{
foreach (var container in containers)
{
diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
index 681bea865a..8a70aa7307 100644
--- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
+++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
@@ -78,7 +78,7 @@ namespace Avalonia.Controls.Generators
var template = GetTreeDataTemplate(item, ItemTemplate);
var result = new T();
- result.SetValue(ContentProperty, template.Build(item));
+ result.SetValue(ContentProperty, template.Build(item), BindingPriority.Style);
var itemsSelector = template.ItemsSelector(item);
@@ -99,25 +99,30 @@ namespace Avalonia.Controls.Generators
}
}
- public override IEnumerable Clear()
+ public override IEnumerable Clear()
{
var items = base.Clear();
Index.Remove(items);
return items;
}
- public override IEnumerable Dematerialize(int startingIndex, int count)
+ public override IEnumerable Dematerialize(int startingIndex, int count)
{
Index.Remove(GetContainerRange(startingIndex, count));
return base.Dematerialize(startingIndex, count);
}
- public override IEnumerable RemoveRange(int startingIndex, int count)
+ public override IEnumerable RemoveRange(int startingIndex, int count)
{
Index.Remove(GetContainerRange(startingIndex, count));
return base.RemoveRange(startingIndex, count);
}
+ public override bool TryRecycle(int oldIndex, int newIndex, object item, IMemberSelector selector)
+ {
+ return false;
+ }
+
///
/// Gets the data template for the specified item.
///
diff --git a/src/Avalonia.Controls/IScrollable.cs b/src/Avalonia.Controls/IScrollable.cs
new file mode 100644
index 0000000000..9bbd0d8518
--- /dev/null
+++ b/src/Avalonia.Controls/IScrollable.cs
@@ -0,0 +1,29 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls.Primitives
+{
+ ///
+ /// Interface implemented by scrollable controls.
+ ///
+ public interface IScrollable
+ {
+ ///
+ /// Gets the extent of the scrollable content, in logical units
+ ///
+ Size Extent { get; }
+
+ ///
+ /// Gets or sets the current scroll offset, in logical units.
+ ///
+ Vector Offset { get; set; }
+
+ ///
+ /// Gets the size of the viewport, in logical units.
+ ///
+ Size Viewport { get; }
+ }
+}
diff --git a/src/Avalonia.Controls/ISetInheritanceParent.cs b/src/Avalonia.Controls/ISetInheritanceParent.cs
new file mode 100644
index 0000000000..788ab77246
--- /dev/null
+++ b/src/Avalonia.Controls/ISetInheritanceParent.cs
@@ -0,0 +1,22 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Defines an interface through which a 's inheritance parent can be set.
+ ///
+ ///
+ /// You should not usually need to use this interface - it is for advanced scenarios only.
+ /// Additionally, also sets the inheritance parent; this
+ /// interface is only needed where the logical and inheritance parents differ.
+ ///
+ public interface ISetInheritanceParent
+ {
+ ///
+ /// Sets the control's inheritance parent.
+ ///
+ /// The parent.
+ void SetParent(IAvaloniaObject parent);
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Controls/IVirtualizingController.cs b/src/Avalonia.Controls/IVirtualizingController.cs
new file mode 100644
index 0000000000..0b997f4948
--- /dev/null
+++ b/src/Avalonia.Controls/IVirtualizingController.cs
@@ -0,0 +1,22 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Interface implemented by controls that act as controllers for an
+ /// .
+ ///
+ public interface IVirtualizingController
+ {
+ ///
+ /// Called when the 's controls should be updated.
+ ///
+ ///
+ /// The controller should respond to this method being called by either adding
+ /// children up until becomes true or
+ /// removing controls.
+ ///
+ void UpdateControls();
+ }
+}
diff --git a/src/Avalonia.Controls/IVirtualizingPanel.cs b/src/Avalonia.Controls/IVirtualizingPanel.cs
new file mode 100644
index 0000000000..5d35fa1ec8
--- /dev/null
+++ b/src/Avalonia.Controls/IVirtualizingPanel.cs
@@ -0,0 +1,67 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+namespace Avalonia.Controls
+{
+ ///
+ /// A panel that can be used to virtualize items.
+ ///
+ public interface IVirtualizingPanel : IPanel
+ {
+ ///
+ /// Gets or sets the controller for the virtualizing panel.
+ ///
+ ///
+ /// A virtualizing controller is responsible for maintaing the controls in the virtualizing
+ /// panel. This property will be set by the controller when virtualization is initialized.
+ /// Note that this property may remain null if the panel is added to a control that does
+ /// not act as a virtualizing controller.
+ ///
+ IVirtualizingController Controller { get; set; }
+
+ ///
+ /// Gets a value indicating whether the panel is full.
+ ///
+ ///
+ /// This property should return false until enough children are added to fill the space
+ /// passed into the last measure in the direction of scroll. It should be updated
+ /// immediately after a child is added or removed.
+ ///
+ bool IsFull { get; }
+
+ ///
+ /// Gets the number of items that can be removed while keeping the panel full.
+ ///
+ ///
+ /// This property should return the number of children that are completely out of the
+ /// panel's current bounds in the direction of scroll. It should be updated after an
+ /// arrange.
+ ///
+ int OverflowCount { get; }
+
+ ///
+ /// Gets the direction of scroll.
+ ///
+ Orientation ScrollDirection { get; }
+
+ ///
+ /// Gets the average size of the materialized items in the direction of scroll.
+ ///
+ double AverageItemSize { get; }
+
+ ///
+ /// Gets or sets a size in pixels by which the content is overflowing the panel, in the
+ /// direction of scroll.
+ ///
+ ///
+ /// This may be non-zero even when is zero if the last item
+ /// overflows the panel bounds.
+ ///
+ double PixelOverflow { get; }
+
+ ///
+ /// Gets or sets the current pixel offset of the items in the direction of scroll.
+ ///
+ double PixelOffset { get; set; }
+ }
+}
diff --git a/src/Avalonia.Controls/ItemVirtualizationMode.cs b/src/Avalonia.Controls/ItemVirtualizationMode.cs
new file mode 100644
index 0000000000..f17e8c07ad
--- /dev/null
+++ b/src/Avalonia.Controls/ItemVirtualizationMode.cs
@@ -0,0 +1,21 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Describes the item virtualization method to use for a list.
+ ///
+ public enum ItemVirtualizationMode
+ {
+ ///
+ /// Do not virtualize items.
+ ///
+ None,
+
+ ///
+ /// Virtualize items without smooth scrolling.
+ ///
+ Simple,
+ }
+}
diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs
index 3a77f61f1b..8e8603ca24 100644
--- a/src/Avalonia.Controls/ItemsControl.cs
+++ b/src/Avalonia.Controls/ItemsControl.cs
@@ -25,7 +25,6 @@ namespace Avalonia.Controls
///
/// The default value for the property.
///
- [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1202:ElementsMustBeOrderedByAccess", Justification = "Needs to be before or a NullReferenceException is thrown.")]
private static readonly FuncTemplate DefaultPanel =
new FuncTemplate(() => new StackPanel());
@@ -90,6 +89,7 @@ namespace Avalonia.Controls
_itemContainerGenerator.ItemTemplate = ItemTemplate;
_itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e);
_itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e);
+ _itemContainerGenerator.Recycled += (_, e) => OnContainersRecycled(e);
}
}
@@ -265,6 +265,28 @@ namespace Avalonia.Controls
LogicalChildren.RemoveAll(toRemove);
}
+ ///
+ /// Called when containers are recycled for the by its
+ /// .
+ ///
+ /// The details of the containers.
+ protected virtual void OnContainersRecycled(ItemContainerEventArgs e)
+ {
+ var toRemove = new List();
+
+ foreach (var container in e.Containers)
+ {
+ // If the item is its own container, then it will be removed from the logical tree
+ // when it is removed from the Items collection.
+ if (container?.ContainerControl != container?.Item)
+ {
+ toRemove.Add(container.ContainerControl);
+ }
+ }
+
+ LogicalChildren.RemoveAll(toRemove);
+ }
+
///
protected override void OnTemplateChanged(AvaloniaPropertyChangedEventArgs e)
{
diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs
index ded75d708a..ed7a50d9b0 100644
--- a/src/Avalonia.Controls/LayoutTransformControl.cs
+++ b/src/Avalonia.Controls/LayoutTransformControl.cs
@@ -139,7 +139,7 @@ namespace Avalonia.Controls
if (null != TransformRoot)
{
TransformRoot.RenderTransform = _matrixTransform;
- TransformRoot.TransformOrigin = new RelativePoint(0, 0, RelativeUnit.Absolute);
+ TransformRoot.RenderTransformOrigin = new RelativePoint(0, 0, RelativeUnit.Absolute);
}
ApplyLayoutTransform();
diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs
index 0f32bc91da..84e4751d37 100644
--- a/src/Avalonia.Controls/ListBox.cs
+++ b/src/Avalonia.Controls/ListBox.cs
@@ -2,12 +2,11 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections;
-using System.Collections.Generic;
-using Avalonia.Collections;
using Avalonia.Controls.Generators;
+using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
using Avalonia.Input;
-using Avalonia.Interactivity;
namespace Avalonia.Controls
{
@@ -16,6 +15,18 @@ namespace Avalonia.Controls
///
public class ListBox : SelectingItemsControl
{
+ ///
+ /// The default value for the property.
+ ///
+ private static readonly FuncTemplate DefaultPanel =
+ new FuncTemplate(() => new VirtualizingStackPanel());
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty ScrollProperty =
+ AvaloniaProperty.RegisterDirect(nameof(Scroll), o => o.Scroll);
+
///
/// Defines the property.
///
@@ -28,6 +39,31 @@ namespace Avalonia.Controls
public static readonly new AvaloniaProperty SelectionModeProperty =
SelectingItemsControl.SelectionModeProperty;
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AvaloniaProperty VirtualizationModeProperty =
+ ItemsPresenter.VirtualizationModeProperty.AddOwner();
+
+ private IScrollable _scroll;
+
+ ///
+ /// Initializes static members of the class.
+ ///
+ static ListBox()
+ {
+ ItemsPanelProperty.OverrideDefaultValue(DefaultPanel);
+ }
+
+ ///
+ /// Gets the scroll information for the .
+ ///
+ public IScrollable Scroll
+ {
+ get { return _scroll; }
+ private set { SetAndRaise(ScrollProperty, ref _scroll, value); }
+ }
+
///
public new IList SelectedItems => base.SelectedItems;
@@ -38,6 +74,15 @@ namespace Avalonia.Controls
set { base.SelectionMode = value; }
}
+ ///
+ /// Gets or sets the virtualization mode for the items.
+ ///
+ public ItemVirtualizationMode VirtualizationMode
+ {
+ get { return GetValue(VirtualizationModeProperty); }
+ set { SetValue(VirtualizationModeProperty, value); }
+ }
+
///
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
@@ -75,5 +120,11 @@ namespace Avalonia.Controls
(e.InputModifiers & InputModifiers.Control) != 0);
}
}
+
+ protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
+ {
+ base.OnTemplateApplied(e);
+ Scroll = e.NameScope.Find("PART_ScrollViewer");
+ }
}
}
diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs
index d5d70ab1ea..2185fd982b 100644
--- a/src/Avalonia.Controls/Menu.cs
+++ b/src/Avalonia.Controls/Menu.cs
@@ -4,6 +4,7 @@
using System;
using System.Linq;
using System.Reactive.Disposables;
+using Avalonia.Controls.Generators;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
@@ -131,6 +132,12 @@ namespace Avalonia.Controls
_subscription.Dispose();
}
+ ///
+ protected override IItemContainerGenerator CreateItemContainerGenerator()
+ {
+ return new ItemContainerGenerator(this, MenuItem.HeaderProperty, null);
+ }
+
///
/// Called when a key is pressed within the menu.
///
diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs
index e4bb44049d..3d15ed99e7 100644
--- a/src/Avalonia.Controls/MenuItem.cs
+++ b/src/Avalonia.Controls/MenuItem.cs
@@ -4,6 +4,7 @@
using System;
using System.Linq;
using System.Windows.Input;
+using Avalonia.Controls.Generators;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
@@ -204,6 +205,12 @@ namespace Avalonia.Controls
IsSelected = true;
}
+ ///
+ protected override IItemContainerGenerator CreateItemContainerGenerator()
+ {
+ return new MenuItemContainerGenerator(this);
+ }
+
///
/// Called when a key is pressed in the .
///
diff --git a/src/Avalonia.Controls/Mixins/SelectableMixin.cs b/src/Avalonia.Controls/Mixins/SelectableMixin.cs
index 0472b604db..2369e9f530 100644
--- a/src/Avalonia.Controls/Mixins/SelectableMixin.cs
+++ b/src/Avalonia.Controls/Mixins/SelectableMixin.cs
@@ -51,22 +51,7 @@ namespace Avalonia.Controls.Mixins
if (sender != null)
{
- var itemsControl = sender.Parent as SelectingItemsControl;
-
- if ((bool)x.NewValue)
- {
- ((IPseudoClasses)sender.Classes).Add(":selected");
-
- if (((IVisual)sender).IsAttachedToVisualTree &&
- itemsControl?.AutoScrollToSelectedItem == true)
- {
- sender.BringIntoView();
- }
- }
- else
- {
- ((IPseudoClasses)sender.Classes).Remove(":selected");
- }
+ ((IPseudoClasses)sender.Classes).Set(":selected", (bool)x.NewValue);
sender.RaiseEvent(new RoutedEventArgs
{
diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs
index 55bdbc9dd4..793399841c 100644
--- a/src/Avalonia.Controls/Panel.cs
+++ b/src/Avalonia.Controls/Panel.cs
@@ -79,12 +79,28 @@ namespace Avalonia.Controls
set { SetValue(BackgroundProperty, value); }
}
+ ///
+ /// Renders the visual to a .
+ ///
+ /// The drawing context.
+ public override void Render(DrawingContext context)
+ {
+ var background = Background;
+ if (background != null)
+ {
+ var renderSize = Bounds.Size;
+ context.FillRectangle(background, new Rect(renderSize));
+ }
+
+ base.Render(context);
+ }
+
///
/// Called when the collection changes.
///
/// The event sender.
/// The event args.
- private void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e)
+ protected virtual void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e)
{
List controls;
@@ -122,21 +138,5 @@ namespace Avalonia.Controls
InvalidateMeasure();
}
-
- ///
- /// Renders the visual to a .
- ///
- /// The drawing context.
- public override void Render(DrawingContext context)
- {
- var background = Background;
- if (background != null)
- {
- var renderSize = Bounds.Size;
- context.FillRectangle(background, new Rect(renderSize));
- }
-
- base.Render(context);
- }
}
}
diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs
index a4f9c0cb43..8b408002a1 100644
--- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs
@@ -95,9 +95,8 @@ namespace Avalonia.Controls.Presenters
}
///
- protected override void CreatePanel()
+ protected override void PanelCreated(IPanel panel)
{
- base.CreatePanel();
var task = MoveToPage(-1, SelectedIndex);
}
@@ -175,12 +174,9 @@ namespace Avalonia.Controls.Presenters
if (container == null)
{
var item = Items.Cast().ElementAt(index);
- var materialized = ItemContainerGenerator.Materialize(
- index,
- new[] { item },
- MemberSelector);
- container = materialized.First().ContainerControl;
- Panel.Children.Add(container);
+ var materialized = ItemContainerGenerator.Materialize(index, item, MemberSelector);
+ Panel.Children.Add(materialized.ContainerControl);
+ container = materialized.ContainerControl;
}
return container;
diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
index 3470a43c1c..4c5a488003 100644
--- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
@@ -80,6 +80,7 @@ namespace Avalonia.Controls.Presenters
private IControl _child;
private bool _createdChild;
+ private IDataTemplate _dataTemplate;
///
/// Initializes static members of the class.
@@ -95,10 +96,6 @@ namespace Avalonia.Controls.Presenters
///
public ContentPresenter()
{
- var dataContext = this.GetObservable(ContentProperty)
- .Select(x => x is IControl ? AvaloniaProperty.UnsetValue : x);
-
- Bind(Control.DataContextProperty, dataContext);
}
///
@@ -200,6 +197,13 @@ namespace Avalonia.Controls.Presenters
}
}
+ ///
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToVisualTree(e);
+ _dataTemplate = null;
+ }
+
///
/// Updates the control based on the control's .
///
@@ -213,34 +217,86 @@ namespace Avalonia.Controls.Presenters
///
public void UpdateChild()
{
- var old = Child;
var content = Content;
- var result = this.MaterializeDataTemplate(content, ContentTemplate);
+ var oldChild = Child;
+ var newChild = content as IControl;
+
+ if (content != null && newChild == null)
+ {
+ // We have content and it isn't a control, so first try to recycle the existing
+ // child control to display the new data by querying if the template that created
+ // the child can recycle items and that it also matches the new data.
+ if (oldChild != null &&
+ _dataTemplate != null &&
+ _dataTemplate.SupportsRecycling &&
+ _dataTemplate.Match(content))
+ {
+ newChild = oldChild;
+ }
+ else
+ {
+ // We couldn't recycle an existing control so find a data template for the data
+ // and use it to create a control.
+ _dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default;
+ newChild = _dataTemplate.Build(content);
+
+ // Try to give the new control its own name scope.
+ var controlResult = newChild as Control;
+
+ if (controlResult != null)
+ {
+ NameScope.SetNameScope(controlResult, new NameScope());
+ }
+ }
+ }
+ else
+ {
+ _dataTemplate = null;
+ }
+
+ // Remove the old child if we're not recycling it.
+ if (oldChild != null && newChild != oldChild)
+ {
+ VisualChildren.Remove(oldChild);
+ }
- if (old != null)
+ // Set the DataContext if the data isn't a control.
+ if (!(content is IControl))
{
- VisualChildren.Remove(old);
+ DataContext = content;
}
- if (result != null)
+ // Update the Child.
+ if (newChild == null)
+ {
+ Child = null;
+ }
+ else if (newChild != oldChild)
{
- if (!(content is IControl))
+ ((ISetInheritanceParent)newChild).SetParent(this);
+
+ Child = newChild;
+
+ if (oldChild?.Parent == this)
{
- result.DataContext = content;
+ LogicalChildren.Remove(oldChild);
}
- Child = result;
-
- if (result.Parent == null)
+ if (newChild.Parent == null)
{
- ((ISetLogicalParent)result).SetParent((ILogical)this.TemplatedParent ?? this);
+ var templatedLogicalParent = TemplatedParent as ILogical;
+
+ if (templatedLogicalParent != null)
+ {
+ ((ISetLogicalParent)newChild).SetParent(templatedLogicalParent);
+ }
+ else
+ {
+ LogicalChildren.Add(newChild);
+ }
}
- VisualChildren.Add(result);
- }
- else
- {
- Child = null;
+ VisualChildren.Add(newChild);
}
_createdChild = true;
diff --git a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs
index 95df903ed3..42311dc781 100644
--- a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs
@@ -6,5 +6,7 @@ namespace Avalonia.Controls.Presenters
public interface IItemsPresenter : IPresenter
{
IPanel Panel { get; }
+
+ void ScrollIntoView(object item);
}
}
diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
new file mode 100644
index 0000000000..964ce82849
--- /dev/null
+++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
@@ -0,0 +1,198 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections;
+using System.Collections.Specialized;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Utils;
+using Avalonia.Input;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls.Presenters
+{
+ ///
+ /// Base class for classes which handle virtualization for an .
+ ///
+ internal abstract class ItemVirtualizer : IVirtualizingController, IDisposable
+ {
+ private bool disposedValue;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ public ItemVirtualizer(ItemsPresenter owner)
+ {
+ Owner = owner;
+ Items = owner.Items;
+ ItemCount = owner.Items.Count();
+ }
+
+ ///
+ /// Gets the which owns the virtualizer.
+ ///
+ public ItemsPresenter Owner { get; }
+
+ ///
+ /// Gets the which will host the items.
+ ///
+ public IVirtualizingPanel VirtualizingPanel => Owner.Panel as IVirtualizingPanel;
+
+ ///
+ /// Gets the items to display.
+ ///
+ public IEnumerable Items { get; private set; }
+
+ ///
+ /// Gets the number of items in .
+ ///
+ public int ItemCount { get; private set; }
+
+ ///
+ /// Gets or sets the index of the first item displayed in the panel.
+ ///
+ public int FirstIndex { get; protected set; }
+
+ ///
+ /// Gets or sets the index of the first item beyond those displayed in the panel.
+ ///
+ public int NextIndex { get; protected set; }
+
+ ///
+ /// Gets a value indicating whether the items should be scroll horizontally or vertically.
+ ///
+ public bool Vertical => VirtualizingPanel.ScrollDirection == Orientation.Vertical;
+
+ ///
+ /// Gets a value indicating whether logical scrolling is enabled.
+ ///
+ public abstract bool IsLogicalScrollEnabled { get; }
+
+ ///
+ /// Gets the value of the scroll extent.
+ ///
+ public abstract double ExtentValue { get; }
+
+ ///
+ /// Gets or sets the value of the current scroll offset.
+ ///
+ public abstract double OffsetValue { get; set; }
+
+ ///
+ /// Gets the value of the scrollable viewport.
+ ///
+ public abstract double ViewportValue { get; }
+
+ ///
+ /// Gets the as a .
+ ///
+ public Size Extent => Vertical ? new Size(0, ExtentValue) : new Size(ExtentValue, 0);
+
+ ///
+ /// Gets the as a .
+ ///
+ public Size Viewport => Vertical ? new Size(0, ViewportValue) : new Size(ViewportValue, 0);
+
+ ///
+ /// Gets or sets the as a .
+ ///
+ public Vector Offset
+ {
+ get
+ {
+ return Vertical ? new Vector(0, OffsetValue) : new Vector(OffsetValue, 0);
+ }
+
+ set
+ {
+ OffsetValue = Vertical ? value.Y : value.X;
+ }
+ }
+
+ ///
+ /// Creates an based on an item presenter's
+ /// .
+ ///
+ /// The items presenter.
+ /// An .
+ public static ItemVirtualizer Create(ItemsPresenter owner)
+ {
+ var virtualizingPanel = owner.Panel as IVirtualizingPanel;
+ var scrollable = (ILogicalScrollable)owner;
+ ItemVirtualizer result = null;
+
+ if (virtualizingPanel != null && scrollable.InvalidateScroll != null)
+ {
+ switch (owner.VirtualizationMode)
+ {
+ case ItemVirtualizationMode.Simple:
+ result = new ItemVirtualizerSimple(owner);
+ break;
+ }
+ }
+
+ if (result == null)
+ {
+ result = new ItemVirtualizerNone(owner);
+ }
+
+ if (virtualizingPanel != null)
+ {
+ virtualizingPanel.Controller = result;
+ }
+
+ return result;
+ }
+
+ ///
+ public virtual void UpdateControls()
+ {
+ }
+
+ ///
+ /// Gets the next control in the specified direction.
+ ///
+ /// The movement direction.
+ /// The control from which movement begins.
+ /// The control.
+ public virtual IControl GetControlInDirection(NavigationDirection direction, IControl from)
+ {
+ return null;
+ }
+
+ ///
+ /// Called when the items for the presenter change, either because
+ /// has been set, the items collection has been
+ /// modified, or the panel has been created.
+ ///
+ /// The items.
+ /// A description of the change.
+ public virtual void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e)
+ {
+ Items = items;
+ ItemCount = items.Count();
+ }
+
+ ///
+ /// Scrolls the specified item into view.
+ ///
+ /// The item.
+ public virtual void ScrollIntoView(object item)
+ {
+ }
+
+ ///
+ public virtual void Dispose()
+ {
+ VirtualizingPanel.Controller = null;
+ VirtualizingPanel.Children.Clear();
+ Owner.ItemContainerGenerator.Clear();
+ }
+
+ ///
+ /// Invalidates the current scroll.
+ ///
+ protected void InvalidateScroll() => ((ILogicalScrollable)Owner).InvalidateScroll();
+ }
+}
diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs
new file mode 100644
index 0000000000..411f309368
--- /dev/null
+++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs
@@ -0,0 +1,173 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using Avalonia.Controls.Generators;
+using Avalonia.Controls.Utils;
+
+namespace Avalonia.Controls.Presenters
+{
+ ///
+ /// Represents an item virtualizer for an that doesn't actually
+ /// virtualize items - it just creates a container for every item.
+ ///
+ internal class ItemVirtualizerNone : ItemVirtualizer
+ {
+ public ItemVirtualizerNone(ItemsPresenter owner)
+ : base(owner)
+ {
+ if (Items != null && owner.Panel != null)
+ {
+ AddContainers(0, Items);
+ }
+ }
+
+ ///
+ public override bool IsLogicalScrollEnabled => false;
+
+ ///
+ /// This property should never be accessed because is
+ /// false.
+ ///
+ public override double ExtentValue
+ {
+ get { throw new NotSupportedException(); }
+ }
+
+ ///
+ /// This property should never be accessed because is
+ /// false.
+ ///
+ public override double OffsetValue
+ {
+ get { throw new NotSupportedException(); }
+ set { throw new NotSupportedException(); }
+ }
+
+ ///
+ /// This property should never be accessed because is
+ /// false.
+ ///
+ public override double ViewportValue
+ {
+ get { throw new NotSupportedException(); }
+ }
+
+ ///
+ public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e)
+ {
+ base.ItemsChanged(items, e);
+
+ var generator = Owner.ItemContainerGenerator;
+ var panel = Owner.Panel;
+
+ switch (e.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ if (e.NewStartingIndex + e.NewItems.Count < Items.Count())
+ {
+ generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count);
+ }
+
+ AddContainers(e.NewStartingIndex, e.NewItems);
+ break;
+
+ case NotifyCollectionChangedAction.Remove:
+ RemoveContainers(generator.RemoveRange(e.OldStartingIndex, e.OldItems.Count));
+ break;
+
+ case NotifyCollectionChangedAction.Replace:
+ RemoveContainers(generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count));
+ var containers = AddContainers(e.NewStartingIndex, e.NewItems);
+
+ var i = e.NewStartingIndex;
+
+ foreach (var container in containers)
+ {
+ panel.Children[i++] = container.ContainerControl;
+ }
+
+ break;
+
+ case NotifyCollectionChangedAction.Move:
+ // TODO: Handle move in a more efficient manner. At the moment we just
+ // drop through to Reset to recreate all the containers.
+
+ case NotifyCollectionChangedAction.Reset:
+ RemoveContainers(generator.Clear());
+
+ if (Items != null)
+ {
+ AddContainers(0, Items);
+ }
+
+ break;
+ }
+
+ Owner.InvalidateMeasure();
+ }
+
+ ///
+ /// Scrolls the specified item into view.
+ ///
+ /// The item.
+ public override void ScrollIntoView(object item)
+ {
+ if (Items != null)
+ {
+ var index = Items.IndexOf(item);
+
+ if (index != -1)
+ {
+ var container = Owner.ItemContainerGenerator.ContainerFromIndex(index);
+ container.BringIntoView();
+ }
+ }
+ }
+
+ private IList AddContainers(int index, IEnumerable items)
+ {
+ var generator = Owner.ItemContainerGenerator;
+ var result = new List();
+ var panel = Owner.Panel;
+
+ foreach (var item in items)
+ {
+ var i = generator.Materialize(index++, item, Owner.MemberSelector);
+
+ if (i.ContainerControl != null)
+ {
+ if (i.Index < panel.Children.Count)
+ {
+ // TODO: This will insert at the wrong place when there are null items.
+ panel.Children.Insert(i.Index, i.ContainerControl);
+ }
+ else
+ {
+ panel.Children.Add(i.ContainerControl);
+ }
+ }
+
+ result.Add(i);
+ }
+
+ return result;
+ }
+
+ private void RemoveContainers(IEnumerable items)
+ {
+ var panel = Owner.Panel;
+
+ foreach (var i in items)
+ {
+ if (i.ContainerControl != null)
+ {
+ panel.Children.Remove(i.ContainerControl);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
new file mode 100644
index 0000000000..27a29a1e04
--- /dev/null
+++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
@@ -0,0 +1,504 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections;
+using System.Collections.Specialized;
+using System.Linq;
+using Avalonia.Controls.Utils;
+using Avalonia.Input;
+using Avalonia.Layout;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls.Presenters
+{
+ ///
+ /// Handles virtualization in an for
+ /// .
+ ///
+ internal class ItemVirtualizerSimple : ItemVirtualizer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ public ItemVirtualizerSimple(ItemsPresenter owner)
+ : base(owner)
+ {
+ // Don't need to add children here as UpdateControls should be called by the panel
+ // measure/arrange.
+ }
+
+ ///
+ public override bool IsLogicalScrollEnabled => true;
+
+ ///
+ public override double ExtentValue => ItemCount;
+
+ ///
+ public override double OffsetValue
+ {
+ get
+ {
+ var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0;
+ return FirstIndex + offset;
+ }
+
+ set
+ {
+ var panel = VirtualizingPanel;
+ var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0;
+ var delta = (int)(value - (FirstIndex + offset));
+
+ if (delta != 0)
+ {
+ var newLastIndex = (NextIndex - 1) + delta;
+
+ if (newLastIndex < ItemCount)
+ {
+ if (panel.PixelOffset > 0)
+ {
+ panel.PixelOffset = 0;
+ delta += 1;
+ }
+
+ if (delta != 0)
+ {
+ RecycleContainersForMove(delta);
+ }
+ }
+ else
+ {
+ // We're moving to a partially obscured item at the end of the list so
+ // offset the panel by the height of the first item.
+ var firstIndex = ItemCount - panel.Children.Count;
+ RecycleContainersForMove(firstIndex - FirstIndex);
+
+ panel.PixelOffset = VirtualizingPanel.ScrollDirection == Orientation.Vertical ?
+ panel.Children[0].Bounds.Height :
+ panel.Children[0].Bounds.Width;
+ }
+ }
+ }
+ }
+
+ ///
+ public override double ViewportValue
+ {
+ get
+ {
+ // If we can't fit the last item in the panel fully, subtract 1 from the viewport.
+ var overflow = VirtualizingPanel.PixelOverflow > 0 ? 1 : 0;
+ return VirtualizingPanel.Children.Count - overflow;
+ }
+ }
+
+ ///
+ public override void UpdateControls()
+ {
+ CreateAndRemoveContainers();
+ InvalidateScroll();
+ }
+
+ ///
+ public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e)
+ {
+ base.ItemsChanged(items, e);
+
+ var panel = VirtualizingPanel;
+
+ if (items != null)
+ {
+ switch (e.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ CreateAndRemoveContainers();
+
+ if (e.NewStartingIndex >= FirstIndex &&
+ e.NewStartingIndex + e.NewItems.Count <= NextIndex)
+ {
+ RecycleContainers();
+ }
+
+ break;
+
+ case NotifyCollectionChangedAction.Remove:
+ if (e.OldStartingIndex >= FirstIndex &&
+ e.OldStartingIndex + e.OldItems.Count <= NextIndex)
+ {
+ RecycleContainersOnRemove();
+ }
+
+ break;
+
+ case NotifyCollectionChangedAction.Move:
+ case NotifyCollectionChangedAction.Replace:
+ RecycleContainers();
+ break;
+
+ case NotifyCollectionChangedAction.Reset:
+ RecycleContainersOnRemove();
+ CreateAndRemoveContainers();
+ break;
+ }
+ }
+ else
+ {
+ Owner.ItemContainerGenerator.Clear();
+ VirtualizingPanel.Children.Clear();
+ FirstIndex = NextIndex = 0;
+ }
+
+ // If we are scrolled to view a partially visible last item but controls were added
+ // then we need to return to a non-offset scroll position.
+ if (panel.PixelOffset != 0 && FirstIndex + panel.Children.Count < ItemCount)
+ {
+ panel.PixelOffset = 0;
+ RecycleContainersForMove(1);
+ }
+
+ InvalidateScroll();
+ }
+
+ public override IControl GetControlInDirection(NavigationDirection direction, IControl from)
+ {
+ var generator = Owner.ItemContainerGenerator;
+ var panel = VirtualizingPanel;
+ var itemIndex = generator.IndexFromContainer(from);
+ var vertical = VirtualizingPanel.ScrollDirection == Orientation.Vertical;
+
+ if (itemIndex == -1)
+ {
+ return null;
+ }
+
+ var newItemIndex = -1;
+
+ switch (direction)
+ {
+ case NavigationDirection.First:
+ newItemIndex = 0;
+ break;
+
+ case NavigationDirection.Last:
+ newItemIndex = ItemCount - 1;
+ break;
+
+ case NavigationDirection.Up:
+ if (vertical)
+ {
+ newItemIndex = itemIndex - 1;
+ }
+
+ break;
+ case NavigationDirection.Down:
+ if (vertical)
+ {
+ newItemIndex = itemIndex + 1;
+ }
+
+ break;
+
+ case NavigationDirection.Left:
+ if (!vertical)
+ {
+ newItemIndex = itemIndex - 1;
+ }
+ break;
+
+ case NavigationDirection.Right:
+ if (!vertical)
+ {
+ newItemIndex = itemIndex + 1;
+ }
+ break;
+
+ case NavigationDirection.PageUp:
+ newItemIndex = Math.Max(0, itemIndex - (int)ViewportValue);
+ break;
+
+ case NavigationDirection.PageDown:
+ newItemIndex = Math.Min(ItemCount - 1, itemIndex + (int)ViewportValue);
+ break;
+ }
+
+ return ScrollIntoView(newItemIndex);
+ }
+
+ ///
+ public override void ScrollIntoView(object item)
+ {
+ var index = Items.IndexOf(item);
+
+ if (index != -1)
+ {
+ ScrollIntoView(index);
+ }
+ }
+
+ ///
+ /// Creates and removes containers such that we have at most enough containers to fill
+ /// the panel.
+ ///
+ private void CreateAndRemoveContainers()
+ {
+ var generator = Owner.ItemContainerGenerator;
+ var panel = VirtualizingPanel;
+
+ if (!panel.IsFull && Items != null)
+ {
+ var memberSelector = Owner.MemberSelector;
+ var index = NextIndex;
+ var step = 1;
+
+ while (!panel.IsFull)
+ {
+ if (index >= ItemCount)
+ {
+ // We can fit more containers in the panel, but we're at the end of the
+ // items. If we're scrolled to the top (FirstIndex == 0), then there are
+ // no more items to create. Otherwise, go backwards adding containers to
+ // the beginning of the panel.
+ if (FirstIndex == 0)
+ {
+ break;
+ }
+ else
+ {
+ index = FirstIndex - 1;
+ step = -1;
+ }
+ }
+
+ var materialized = generator.Materialize(index, Items.ElementAt(index), memberSelector);
+
+ if (step == 1)
+ {
+ panel.Children.Add(materialized.ContainerControl);
+ }
+ else
+ {
+ panel.Children.Insert(0, materialized.ContainerControl);
+ }
+
+ index += step;
+ }
+
+ if (step == 1)
+ {
+ NextIndex = index;
+ }
+ else
+ {
+ NextIndex = ItemCount;
+ FirstIndex = index + 1;
+ }
+ }
+
+ if (panel.OverflowCount > 0)
+ {
+ RemoveContainers(panel.OverflowCount);
+ }
+ }
+
+ ///
+ /// Updates the containers in the panel to make sure they are displaying the correct item
+ /// based on .
+ ///
+ ///
+ /// This method requires that + the number of
+ /// materialized containers is not more than .
+ ///
+ private void RecycleContainers()
+ {
+ var panel = VirtualizingPanel;
+ var generator = Owner.ItemContainerGenerator;
+ var selector = Owner.MemberSelector;
+ var containers = generator.Containers.ToList();
+ var itemIndex = FirstIndex;
+
+ foreach (var container in containers)
+ {
+ var item = Items.ElementAt(itemIndex);
+
+ if (!object.Equals(container.Item, item))
+ {
+ if (!generator.TryRecycle(itemIndex, itemIndex, item, selector))
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ ++itemIndex;
+ }
+ }
+
+ ///
+ /// Recycles containers when a move occurs.
+ ///
+ /// The delta of the move.
+ ///
+ /// If the move is less than a page, then this method moves the containers for the items
+ /// that are still visible to the correct place, and recyles and moves the others. For
+ /// example: if there are 20 items and 10 containers visible and the user scrolls 5
+ /// items down, then the bottom 5 containers will be moved to the top and the top 5 will
+ /// be moved to the bottom and recycled to display the newly visible item. Updates
+ /// and
+ /// with their new values.
+ ///
+ private void RecycleContainersForMove(int delta)
+ {
+ var panel = VirtualizingPanel;
+ var generator = Owner.ItemContainerGenerator;
+ var selector = Owner.MemberSelector;
+ var sign = delta < 0 ? -1 : 1;
+ var count = Math.Min(Math.Abs(delta), panel.Children.Count);
+ var move = count < panel.Children.Count;
+ var first = delta < 0 && move ? panel.Children.Count + delta : 0;
+ var containers = panel.Children.GetRange(first, count).ToList();
+
+ for (var i = 0; i < count; ++i)
+ {
+ var oldItemIndex = FirstIndex + first + i;
+ var newItemIndex = oldItemIndex + delta + ((panel.Children.Count - count) * sign);
+
+ var item = Items.ElementAt(newItemIndex);
+
+ if (!generator.TryRecycle(oldItemIndex, newItemIndex, item, selector))
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ if (move)
+ {
+ if (delta > 0)
+ {
+ panel.Children.MoveRange(first, count, panel.Children.Count);
+ }
+ else
+ {
+ panel.Children.MoveRange(first, count, 0);
+ }
+ }
+
+ FirstIndex += delta;
+ NextIndex += delta;
+ }
+
+ ///
+ /// Recycles containers due to items being removed.
+ ///
+ private void RecycleContainersOnRemove()
+ {
+ var panel = VirtualizingPanel;
+
+ if (NextIndex <= ItemCount)
+ {
+ // Items have been removed but FirstIndex..NextIndex is still a valid range in the
+ // items, so just recycle the containers to adapt to the new state.
+ RecycleContainers();
+ }
+ else
+ {
+ // Items have been removed and now the range FirstIndex..NextIndex goes out of
+ // the item bounds. Remove any excess containers, try to scroll up and then recycle
+ // the containers to make sure they point to the correct item.
+ var newFirstIndex = Math.Max(0, FirstIndex - (NextIndex - ItemCount));
+ var delta = newFirstIndex - FirstIndex;
+ var newNextIndex = NextIndex + delta;
+
+ if (newNextIndex > ItemCount)
+ {
+ RemoveContainers(newNextIndex - ItemCount);
+ }
+
+ if (delta != 0)
+ {
+ RecycleContainersForMove(delta);
+ }
+
+ RecycleContainers();
+ }
+ }
+
+ ///
+ /// Removes the specified number of containers from the end of the panel and updates
+ /// .
+ ///
+ /// The number of containers to remove.
+ private void RemoveContainers(int count)
+ {
+ var index = VirtualizingPanel.Children.Count - count;
+
+ VirtualizingPanel.Children.RemoveRange(index, count);
+ Owner.ItemContainerGenerator.Dematerialize(FirstIndex + index, count);
+ NextIndex -= count;
+ }
+
+ ///
+ /// Scrolls the item with the specified index into view.
+ ///
+ /// The item index.
+ /// The container that was brought into view.
+ private IControl ScrollIntoView(int index)
+ {
+ var panel = VirtualizingPanel;
+ var generator = Owner.ItemContainerGenerator;
+ var newOffset = -1.0;
+
+ if (index >= 0 && index < ItemCount)
+ {
+ if (index < FirstIndex)
+ {
+ newOffset = index;
+ }
+ else if (index >= NextIndex)
+ {
+ newOffset = index - Math.Ceiling(ViewportValue - 1);
+ }
+ else if (OffsetValue + ViewportValue >= ItemCount)
+ {
+ newOffset = OffsetValue - 1;
+ }
+
+ if (newOffset != -1)
+ {
+ OffsetValue = newOffset;
+ }
+
+ var container = generator.ContainerFromIndex(index);
+ var layoutManager = LayoutManager.Instance;
+
+ // We need to do a layout here because it's possible that the container we moved to
+ // is only partially visible due to differing item sizes. If the container is only
+ // partially visible, scroll again. Don't do this if there's no layout manager:
+ // it means we're running a unit test.
+ if (layoutManager != null)
+ {
+ layoutManager.ExecuteLayoutPass();
+
+ if (!new Rect(panel.Bounds.Size).Contains(container.Bounds))
+ {
+ OffsetValue += 1;
+ }
+ }
+
+ return container;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Ensures an offset value is within the value range.
+ ///
+ /// The value.
+ /// The coerced value.
+ private double CoerceOffset(double value)
+ {
+ var max = Math.Max(ExtentValue - ViewportValue, 0);
+ return MathUtilities.Clamp(value, 0, max);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs
index 8c8ab5c0f8..ffc2680c83 100644
--- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs
@@ -1,19 +1,29 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
-using System.Collections.Generic;
+using System;
using System.Collections.Specialized;
-using Avalonia.Controls.Generators;
-using Avalonia.Controls.Utils;
+using Avalonia.Controls.Primitives;
using Avalonia.Input;
+using static Avalonia.Utilities.MathUtilities;
namespace Avalonia.Controls.Presenters
{
///
/// Displays items inside an .
///
- public class ItemsPresenter : ItemsPresenterBase
+ public class ItemsPresenter : ItemsPresenterBase, ILogicalScrollable
{
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty VirtualizationModeProperty =
+ AvaloniaProperty.Register(
+ nameof(VirtualizationMode),
+ defaultValue: ItemVirtualizationMode.Simple);
+
+ private ItemVirtualizer _virtualizer;
+
///
/// Initializes static members of the class.
///
@@ -22,127 +32,101 @@ namespace Avalonia.Controls.Presenters
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(
typeof(ItemsPresenter),
KeyboardNavigationMode.Once);
+
+ VirtualizationModeProperty.Changed
+ .AddClassHandler(x => x.VirtualizationModeChanged);
}
- ///
- protected override void CreatePanel()
+ ///
+ /// Gets or sets the virtualization mode for the items.
+ ///
+ public ItemVirtualizationMode VirtualizationMode
{
- base.CreatePanel();
-
- if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty))
- {
- KeyboardNavigation.SetDirectionalNavigation(
- (InputElement)Panel,
- KeyboardNavigationMode.Contained);
- }
-
- KeyboardNavigation.SetTabNavigation(
- (InputElement)Panel,
- KeyboardNavigation.GetTabNavigation(this));
+ get { return GetValue(VirtualizationModeProperty); }
+ set { SetValue(VirtualizationModeProperty, value); }
}
///
- protected override void ItemsChanged(NotifyCollectionChangedEventArgs e)
+ bool ILogicalScrollable.IsLogicalScrollEnabled
{
- var generator = ItemContainerGenerator;
+ get { return _virtualizer?.IsLogicalScrollEnabled ?? false; }
+ }
- // TODO: Handle Move and Replace etc.
- switch (e.Action)
- {
- case NotifyCollectionChangedAction.Add:
- if (e.NewStartingIndex + e.NewItems.Count < Items.Count())
- {
- generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count);
- }
+ ///
+ Size IScrollable.Extent => _virtualizer.Extent;
- AddContainers(generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector));
- break;
+ ///
+ Vector IScrollable.Offset
+ {
+ get { return _virtualizer.Offset; }
+ set { _virtualizer.Offset = CoerceOffset(value); }
+ }
+
+ ///
+ Size IScrollable.Viewport => _virtualizer.Viewport;
- case NotifyCollectionChangedAction.Remove:
- RemoveContainers(generator.RemoveRange(e.OldStartingIndex, e.OldItems.Count));
- break;
+ ///
+ Action ILogicalScrollable.InvalidateScroll { get; set; }
- case NotifyCollectionChangedAction.Replace:
- RemoveContainers(generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count));
- var containers = generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector);
- AddContainers(containers);
+ ///
+ Size ILogicalScrollable.ScrollSize => new Size(1, 1);
- var i = e.NewStartingIndex;
+ ///
+ Size ILogicalScrollable.PageScrollSize => new Size(0, 1);
- foreach (var container in containers)
- {
- Panel.Children[i++] = container.ContainerControl;
- }
+ ///
+ bool ILogicalScrollable.BringIntoView(IControl target, Rect targetRect)
+ {
+ return false;
+ }
- break;
+ ///
+ IControl ILogicalScrollable.GetControlInDirection(NavigationDirection direction, IControl from)
+ {
+ return _virtualizer?.GetControlInDirection(direction, from);
+ }
- case NotifyCollectionChangedAction.Move:
- // TODO: Implement Move in a more efficient manner.
- case NotifyCollectionChangedAction.Reset:
- RemoveContainers(generator.Clear());
+ public override void ScrollIntoView(object item)
+ {
+ _virtualizer?.ScrollIntoView(item);
+ }
- if (Items != null)
- {
- AddContainers(generator.Materialize(0, Items, MemberSelector));
- }
+ ///
+ protected override void PanelCreated(IPanel panel)
+ {
+ _virtualizer = ItemVirtualizer.Create(this);
+ ((ILogicalScrollable)this).InvalidateScroll?.Invoke();
- break;
+ if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty))
+ {
+ KeyboardNavigation.SetDirectionalNavigation(
+ (InputElement)Panel,
+ KeyboardNavigationMode.Contained);
}
- InvalidateMeasure();
+ KeyboardNavigation.SetTabNavigation(
+ (InputElement)Panel,
+ KeyboardNavigation.GetTabNavigation(this));
}
- private void AddContainersToPanel(IEnumerable items)
+ protected override void ItemsChanged(NotifyCollectionChangedEventArgs e)
{
- foreach (var i in items)
- {
- if (i.ContainerControl != null)
- {
- if (i.Index < this.Panel.Children.Count)
- {
- // HACK: This will insert at the wrong place when there are null items,
- // but all of this will need to be rewritten when we implement
- // virtualization so hope no-one notices until then :)
- this.Panel.Children.Insert(i.Index, i.ContainerControl);
- }
- else
- {
- this.Panel.Children.Add(i.ContainerControl);
- }
- }
- }
+ _virtualizer?.ItemsChanged(Items, e);
}
- private void AddContainers(IEnumerable items)
+ private Vector CoerceOffset(Vector value)
{
- foreach (var i in items)
- {
- if (i.ContainerControl != null)
- {
- if (i.Index < this.Panel.Children.Count)
- {
- // HACK: This will insert at the wrong place when there are null items,
- // but all of this will need to be rewritten when we implement
- // virtualization so hope no-one notices until then :)
- this.Panel.Children.Insert(i.Index, i.ContainerControl);
- }
- else
- {
- this.Panel.Children.Add(i.ContainerControl);
- }
- }
- }
+ var scrollable = (ILogicalScrollable)this;
+ var maxX = Math.Max(scrollable.Extent.Width - scrollable.Viewport.Width, 0);
+ var maxY = Math.Max(scrollable.Extent.Height - scrollable.Viewport.Height, 0);
+ return new Vector(Clamp(value.X, 0, maxX), Clamp(value.Y, 0, maxY));
}
- private void RemoveContainers(IEnumerable items)
+ private void VirtualizationModeChanged(AvaloniaPropertyChangedEventArgs e)
{
- foreach (var i in items)
- {
- if (i.ContainerControl != null)
- {
- this.Panel.Children.Remove(i.ContainerControl);
- }
- }
+ _virtualizer?.Dispose();
+ _virtualizer = ItemVirtualizer.Create(this);
+ ((ILogicalScrollable)this).InvalidateScroll?.Invoke();
}
}
}
\ No newline at end of file
diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
index 91ffec9c4c..5a56e52029 100644
--- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
+++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
@@ -101,8 +101,7 @@ namespace Avalonia.Controls.Presenters
{
if (_generator == null)
{
- var i = TemplatedParent as ItemsControl;
- _generator = (i?.ItemContainerGenerator) ?? new ItemContainerGenerator(this);
+ _generator = CreateItemContainerGenerator();
}
return _generator;
@@ -164,6 +163,31 @@ namespace Avalonia.Controls.Presenters
}
}
+ ///
+ public virtual void ScrollIntoView(object item)
+ {
+ }
+
+ ///
+ /// Creates the for the control.
+ ///
+ ///
+ /// An or null.
+ ///
+ protected virtual IItemContainerGenerator CreateItemContainerGenerator()
+ {
+ var i = TemplatedParent as ItemsControl;
+ var result = i?.ItemContainerGenerator;
+
+ if (result == null)
+ {
+ result = new ItemContainerGenerator(this);
+ result.ItemTemplate = ItemTemplate;
+ }
+
+ return result;
+ }
+
///
protected override Size MeasureOverride(Size availableSize)
{
@@ -178,11 +202,26 @@ namespace Avalonia.Controls.Presenters
return finalSize;
}
+ ///
+ /// Called when the is created.
+ ///
+ /// The panel.
+ protected virtual void PanelCreated(IPanel panel)
+ {
+ }
+
+ ///
+ /// Called when the items for the presenter change, either because
+ /// has been set, the items collection has been modified, or the panel has been created.
+ ///
+ /// A description of the change.
+ protected abstract void ItemsChanged(NotifyCollectionChangedEventArgs e);
+
///
/// Creates the when is called for the first
/// time.
///
- protected virtual void CreatePanel()
+ private void CreatePanel()
{
Panel = ItemsPanel.Build();
Panel.SetValue(TemplatedParentProperty, TemplatedParent);
@@ -201,16 +240,11 @@ namespace Avalonia.Controls.Presenters
incc.CollectionChanged += ItemsCollectionChanged;
}
+ PanelCreated(Panel);
+
ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
- ///
- /// Called when the items for the presenter change, either because
- /// has been set, or the items collection has been modified.
- ///
- /// A description of the change.
- protected abstract void ItemsChanged(NotifyCollectionChangedEventArgs e);
-
///
/// Called when the collection changes.
///
diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
index 0077497b10..d9f0260e39 100644
--- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
@@ -15,7 +15,7 @@ namespace Avalonia.Controls.Presenters
///
/// Presents a scrolling view of content inside a .
///
- public class ScrollContentPresenter : ContentPresenter, IPresenter
+ public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable
{
///
/// Defines the property.
@@ -50,7 +50,7 @@ namespace Avalonia.Controls.Presenters
private Size _extent;
private Size _measuredExtent;
private Vector _offset;
- private IDisposable _scrollableSubscription;
+ private IDisposable _logicalScrollSubscription;
private Size _viewport;
///
@@ -59,6 +59,7 @@ namespace Avalonia.Controls.Presenters
static ScrollContentPresenter()
{
ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true);
+ ChildProperty.Changed.AddClassHandler(x => x.ChildChanged);
AffectsArrange(OffsetProperty);
}
@@ -69,7 +70,7 @@ namespace Avalonia.Controls.Presenters
{
AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested);
- this.GetObservable(ChildProperty).Subscribe(ChildChanged);
+ this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription);
}
///
@@ -117,6 +118,14 @@ namespace Avalonia.Controls.Presenters
return false;
}
+ var scrollable = Child as ILogicalScrollable;
+ var control = target as IControl;
+
+ if (scrollable?.IsLogicalScrollEnabled == true && control != null)
+ {
+ return scrollable.BringIntoView(control, targetRect);
+ }
+
var transform = target.TransformToVisual(Child);
if (transform == null)
@@ -169,7 +178,7 @@ namespace Avalonia.Controls.Presenters
{
var measureSize = availableSize;
- if (_scrollableSubscription == null)
+ if (_logicalScrollSubscription == null)
{
measureSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
@@ -194,21 +203,25 @@ namespace Avalonia.Controls.Presenters
protected override Size ArrangeOverride(Size finalSize)
{
var child = this.GetVisualChildren().SingleOrDefault() as ILayoutable;
- var offset = default(Vector);
+ var logicalScroll = _logicalScrollSubscription != null;
- if (_scrollableSubscription == null)
+ if (!logicalScroll)
{
Viewport = finalSize;
Extent = _measuredExtent;
- offset = Offset;
- }
- if (child != null)
- {
- var size = new Size(
+ if (child != null)
+ {
+ var size = new Size(
Math.Max(finalSize.Width, child.DesiredSize.Width),
Math.Max(finalSize.Height, child.DesiredSize.Height));
- child.Arrange(new Rect((Point)(-offset), size));
+ child.Arrange(new Rect((Point)(-Offset), size));
+ return finalSize;
+ }
+ }
+ else if (child != null)
+ {
+ child.Arrange(new Rect(finalSize));
return finalSize;
}
@@ -218,26 +231,32 @@ namespace Avalonia.Controls.Presenters
///
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{
- if (Extent.Height > Viewport.Height)
+ if (Extent.Height > Viewport.Height || Extent.Width > Viewport.Width)
{
- var scrollable = Child as IScrollable;
+ var scrollable = Child as ILogicalScrollable;
+ bool isLogical = scrollable?.IsLogicalScrollEnabled == true;
+
+ double x = Offset.X;
+ double y = Offset.Y;
- if (scrollable != null)
- {
- var y = Offset.Y + (-e.Delta.Y * scrollable.ScrollSize.Height);
+ if (Extent.Height > Viewport.Height)
+ {
+ double height = isLogical ? scrollable.ScrollSize.Height : 50;
+ y += -e.Delta.Y * height;
y = Math.Max(y, 0);
y = Math.Min(y, Extent.Height - Viewport.Height);
- Offset = new Vector(Offset.X, y);
- e.Handled = true;
}
- else
+
+ if (Extent.Width > Viewport.Width)
{
- var y = Offset.Y + (-e.Delta.Y * 50);
- y = Math.Max(y, 0);
- y = Math.Min(y, Extent.Height - Viewport.Height);
- Offset = new Vector(Offset.X, y);
- e.Handled = true;
+ double width = isLogical ? scrollable.ScrollSize.Width : 50;
+ x += -e.Delta.X * width;
+ x = Math.Max(x, 0);
+ x = Math.Min(x, Extent.Width - Viewport.Width);
}
+
+ Offset = new Vector(x, y);
+ e.Handled = true;
}
}
@@ -246,28 +265,53 @@ namespace Avalonia.Controls.Presenters
e.Handled = BringDescendentIntoView(e.TargetObject, e.TargetRect);
}
- private void ChildChanged(IControl child)
+ private void ChildChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ UpdateScrollableSubscription((IControl)e.NewValue);
+
+ if (e.OldValue != null)
+ {
+ Offset = default(Vector);
+ }
+ }
+
+ private void UpdateScrollableSubscription(IControl child)
{
- var scrollable = child as IScrollable;
+ var scrollable = child as ILogicalScrollable;
- _scrollableSubscription?.Dispose();
- _scrollableSubscription = null;
+ _logicalScrollSubscription?.Dispose();
+ _logicalScrollSubscription = null;
if (scrollable != null)
{
scrollable.InvalidateScroll = () => UpdateFromScrollable(scrollable);
- _scrollableSubscription = new CompositeDisposable(
- this.GetObservable(OffsetProperty).Skip(1).Subscribe(x => scrollable.Offset = x),
- Disposable.Create(() => scrollable.InvalidateScroll = null));
- UpdateFromScrollable(scrollable);
+
+ if (scrollable.IsLogicalScrollEnabled == true)
+ {
+ _logicalScrollSubscription = new CompositeDisposable(
+ this.GetObservable(OffsetProperty).Skip(1).Subscribe(x => scrollable.Offset = x),
+ Disposable.Create(() => scrollable.InvalidateScroll = null));
+ UpdateFromScrollable(scrollable);
+ }
}
}
- private void UpdateFromScrollable(IScrollable scrollable)
+ private void UpdateFromScrollable(ILogicalScrollable scrollable)
{
- Viewport = scrollable.Viewport;
- Extent = scrollable.Extent;
- Offset = scrollable.Offset;
+ var logicalScroll = _logicalScrollSubscription != null;
+
+ if (logicalScroll != scrollable.IsLogicalScrollEnabled)
+ {
+ UpdateScrollableSubscription(Child);
+ Offset = default(Vector);
+ InvalidateMeasure();
+ }
+ else if (scrollable.IsLogicalScrollEnabled)
+ {
+ Viewport = scrollable.Viewport;
+ Extent = scrollable.Extent;
+ Offset = scrollable.Offset;
+ }
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs
index b732387cf5..1dfa2d7af2 100644
--- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs
+++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs
@@ -60,7 +60,7 @@ namespace Avalonia.Controls.Primitives
if (info != null)
{
child.RenderTransform = new MatrixTransform(info.Bounds.Transform);
- child.TransformOrigin = new RelativePoint(new Point(0,0), RelativeUnit.Absolute);
+ child.RenderTransformOrigin = new RelativePoint(new Point(0,0), RelativeUnit.Absolute);
child.Arrange(info.Bounds.Bounds);
}
else
diff --git a/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs
new file mode 100644
index 0000000000..6c8f463a96
--- /dev/null
+++ b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs
@@ -0,0 +1,69 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using Avalonia.Input;
+
+namespace Avalonia.Controls.Primitives
+{
+ ///
+ /// Interface implemented by controls that handle their own scrolling when placed inside a
+ /// .
+ ///
+ ///
+ /// Controls that implement this interface, when placed inside a
+ /// can override the physical scrolling behavior of the scroll viewer with logical scrolling.
+ /// Physical scrolling means that the scroll viewer is a simple viewport onto a larger canvas
+ /// whereas logical scrolling means that the scrolling is handled by the child control itself
+ /// and it can choose to do handle the scroll information as it sees fit.
+ ///
+ public interface ILogicalScrollable : IScrollable
+ {
+ ///
+ /// Gets a value indicating whether logical scrolling is enabled on the control.
+ ///
+ bool IsLogicalScrollEnabled { get; }
+
+ ///
+ /// Gets or sets the scroll invalidation method.
+ ///
+ ///
+ ///
+ /// This method notifies the attached of a change in
+ /// the , or
+ /// properties.
+ ///
+ ///
+ /// This property is set by the parent when the
+ /// is placed inside it.
+ ///
+ ///
+ Action InvalidateScroll { get; set; }
+
+ ///
+ /// Gets the size to scroll by, in logical units.
+ ///
+ Size ScrollSize { get; }
+
+ ///
+ /// Gets the size to page by, in logical units.
+ ///
+ Size PageScrollSize { get; }
+
+ ///
+ /// Attempts to bring a portion of the target visual into view by scrolling the content.
+ ///
+ /// The target visual.
+ /// The portion of the target visual to bring into view.
+ /// True if the scroll offset was changed; otherwise false.
+ bool BringIntoView(IControl target, Rect targetRect);
+
+ ///
+ /// Gets the next control in the specified direction.
+ ///
+ /// The movement direction.
+ /// The control from which movement begins.
+ /// The control.
+ IControl GetControlInDirection(NavigationDirection direction, IControl from);
+ }
+}
diff --git a/src/Avalonia.Controls/Primitives/IScrollable.cs b/src/Avalonia.Controls/Primitives/IScrollable.cs
deleted file mode 100644
index a5165fcfb4..0000000000
--- a/src/Avalonia.Controls/Primitives/IScrollable.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-
-namespace Avalonia.Controls.Primitives
-{
- ///
- /// Interface implemented by controls that handle their own scrolling when placed inside a
- /// .
- ///
- public interface IScrollable
- {
- ///
- /// Gets or sets the scroll invalidation method.
- ///
- ///
- ///
- /// This method notifies the attached of a change in
- /// the , or properties.
- ///
- ///
- /// This property is set by the parent when the
- /// is placed inside it.
- ///
- ///
- Action InvalidateScroll { get; set; }
-
- ///
- /// Gets the extent of the scrollable content, in logical units
- ///
- Size Extent { get; }
-
- ///
- /// Gets or sets the current scroll offset, in logical units.
- ///
- Vector Offset { get; set; }
-
- ///
- /// Gets the size of the viewport, in logical units.
- ///
- Size Viewport { get; }
-
- ///
- /// Gets the size to scroll by, in logical units.
- ///
- Size ScrollSize { get; }
-
- ///
- /// Gets the size to page by, in logical units.
- ///
- Size PageScrollSize { get; }
- }
-}
diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs
index c24d1cef2d..81d01ff74f 100644
--- a/src/Avalonia.Controls/Primitives/Popup.cs
+++ b/src/Avalonia.Controls/Primitives/Popup.cs
@@ -10,6 +10,7 @@ using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.Rendering;
using Avalonia.VisualTree;
+using Avalonia.Layout;
namespace Avalonia.Controls.Primitives
{
@@ -39,6 +40,18 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty PlacementModeProperty =
AvaloniaProperty.Register(nameof(PlacementMode), defaultValue: PlacementMode.Bottom);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty HorizontalOffsetProperty =
+ AvaloniaProperty.Register(nameof(HorizontalOffset));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty VerticalOffsetProperty =
+ AvaloniaProperty.Register(nameof(VerticalOffset));
+
///
/// Defines the property.
///
@@ -122,6 +135,24 @@ namespace Avalonia.Controls.Primitives
set { SetValue(PlacementModeProperty, value); }
}
+ ///
+ /// Gets or sets the Horizontal offset of the popup in relation to the
+ ///
+ public double HorizontalOffset
+ {
+ get { return GetValue(HorizontalOffsetProperty); }
+ set { SetValue(HorizontalOffsetProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the Vertical offset of the popup in relation to the
+ ///
+ public double VerticalOffset
+ {
+ get { return GetValue(VerticalOffsetProperty); }
+ set { SetValue(VerticalOffsetProperty, value); }
+ }
+
///
/// Gets or sets the control that is used to determine the popup's position.
///
@@ -287,18 +318,26 @@ namespace Avalonia.Controls.Primitives
if (target?.GetVisualRoot() == null)
{
mode = PlacementMode.Pointer;
- }
+ }
switch (mode)
{
case PlacementMode.Pointer:
- return MouseDevice.Instance?.Position ?? default(Point);
+ if (MouseDevice.Instance != null)
+ {
+ // Scales the Horizontal and Vertical offset to screen co-ordinates.
+ var screenOffset = new Point(HorizontalOffset * (PopupRoot as ILayoutRoot).LayoutScaling, VerticalOffset * (PopupRoot as ILayoutRoot).LayoutScaling);
+ return MouseDevice.Instance.Position + screenOffset;
+ }
+
+ return default(Point);
case PlacementMode.Bottom:
- return target?.PointToScreen(new Point(0, target.Bounds.Height)) ?? zero;
+
+ return target?.PointToScreen(new Point(0 + HorizontalOffset, target.Bounds.Height + VerticalOffset)) ?? zero;
case PlacementMode.Right:
- return target?.PointToScreen(new Point(target.Bounds.Width, 0)) ?? zero;
+ return target?.PointToScreen(new Point(target.Bounds.Width + HorizontalOffset, 0 + VerticalOffset)) ?? zero;
default:
throw new InvalidOperationException("Invalid value for Popup.PlacementMode");
diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
index 018507bbc8..27f3407fd9 100644
--- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
+++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
@@ -280,6 +280,12 @@ namespace Avalonia.Controls.Primitives
}
}
+ ///
+ /// Scrolls the specified item into view.
+ ///
+ /// The item.
+ public void ScrollIntoView(object item) => Presenter?.ScrollIntoView(item);
+
///
/// Tries to get the container that was the source of an event.
///
@@ -394,6 +400,19 @@ namespace Avalonia.Controls.Primitives
}
}
+ protected override void OnContainersRecycled(ItemContainerEventArgs e)
+ {
+ foreach (var i in e.Containers)
+ {
+ if (i.ContainerControl != null && i.Item != null)
+ {
+ MarkContainerSelected(
+ i.ContainerControl,
+ SelectedItems.Contains(i.Item));
+ }
+ }
+ }
+
///
protected override void OnDataContextChanging()
{
@@ -710,6 +729,12 @@ namespace Avalonia.Controls.Primitives
{
case NotifyCollectionChangedAction.Add:
SelectedItemsAdded(e.NewItems.Cast().ToList());
+
+ if (AutoScrollToSelectedItem)
+ {
+ ScrollIntoView(e.NewItems[0]);
+ }
+
added = e.NewItems;
break;
diff --git a/src/Avalonia.Controls/Primitives/TabStrip.cs b/src/Avalonia.Controls/Primitives/TabStrip.cs
index 4a6c9af95f..0e15ae4d7b 100644
--- a/src/Avalonia.Controls/Primitives/TabStrip.cs
+++ b/src/Avalonia.Controls/Primitives/TabStrip.cs
@@ -9,6 +9,9 @@ namespace Avalonia.Controls.Primitives
{
public class TabStrip : SelectingItemsControl
{
+ private static readonly FuncTemplate DefaultPanel =
+ new FuncTemplate(() => new WrapPanel { Orientation = Orientation.Horizontal });
+
private static IMemberSelector s_MemberSelector = new FuncMemberSelector(SelectHeader);
static TabStrip()
@@ -16,6 +19,7 @@ namespace Avalonia.Controls.Primitives
MemberSelectorProperty.OverrideDefaultValue(s_MemberSelector);
SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected);
FocusableProperty.OverrideDefaultValue(typeof(TabStrip), false);
+ ItemsPanelProperty.OverrideDefaultValue(DefaultPanel);
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs
index 4f5cf304b2..c52c1ad505 100644
--- a/src/Avalonia.Controls/ScrollViewer.cs
+++ b/src/Avalonia.Controls/ScrollViewer.cs
@@ -12,7 +12,7 @@ namespace Avalonia.Controls
///
/// A control scrolls its content if the content is bigger than the space available.
///
- public class ScrollViewer : ContentControl
+ public class ScrollViewer : ContentControl, IScrollable
{
///
/// Defines the property.
@@ -370,7 +370,7 @@ namespace Avalonia.Controls
private static double Max(double x, double y)
{
var result = Math.Max(x, y);
- return double.IsNaN(result) ? 0 : Math.Round(result);
+ return double.IsNaN(result) ? 0 : result;
}
private static Vector ValidateOffset(AvaloniaObject o, Vector value)
diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs
index 1a032d4def..26a755e5f1 100644
--- a/src/Avalonia.Controls/StackPanel.cs
+++ b/src/Avalonia.Controls/StackPanel.cs
@@ -72,37 +72,52 @@ namespace Avalonia.Controls
/// The movement direction.
/// The control from which movement begins.
/// The control.
- IInputElement INavigableContainer.GetControl(FocusNavigationDirection direction, IInputElement from)
+ IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from)
+ {
+ var fromControl = from as IControl;
+ return (fromControl != null) ? GetControlInDirection(direction, fromControl) : null;
+ }
+
+ ///
+ /// Gets the next control in the specified direction.
+ ///
+ /// The movement direction.
+ /// The control from which movement begins.
+ /// The control.
+ protected virtual IInputElement GetControlInDirection(NavigationDirection direction, IControl from)
{
var horiz = Orientation == Orientation.Horizontal;
int index = Children.IndexOf((IControl)from);
switch (direction)
{
- case FocusNavigationDirection.First:
+ case NavigationDirection.First:
index = 0;
break;
- case FocusNavigationDirection.Last:
+ case NavigationDirection.Last:
index = Children.Count - 1;
break;
- case FocusNavigationDirection.Next:
+ case NavigationDirection.Next:
++index;
break;
- case FocusNavigationDirection.Previous:
+ case NavigationDirection.Previous:
--index;
break;
- case FocusNavigationDirection.Left:
+ case NavigationDirection.Left:
index = horiz ? index - 1 : -1;
break;
- case FocusNavigationDirection.Right:
+ case NavigationDirection.Right:
index = horiz ? index + 1 : -1;
break;
- case FocusNavigationDirection.Up:
+ case NavigationDirection.Up:
index = horiz ? -1 : index - 1;
break;
- case FocusNavigationDirection.Down:
+ case NavigationDirection.Down:
index = horiz ? -1 : index + 1;
break;
+ default:
+ index = -1;
+ break;
}
if (index >= 0 && index < Children.Count)
@@ -181,6 +196,7 @@ namespace Avalonia.Controls
/// The space taken.
protected override Size ArrangeOverride(Size finalSize)
{
+ var orientation = Orientation;
double arrangedWidth = finalSize.Width;
double arrangedHeight = finalSize.Height;
double gap = Gap;
@@ -199,11 +215,11 @@ namespace Avalonia.Controls
double childWidth = child.DesiredSize.Width;
double childHeight = child.DesiredSize.Height;
- if (Orientation == Orientation.Vertical)
+ if (orientation == Orientation.Vertical)
{
double width = Math.Max(childWidth, arrangedWidth);
Rect childFinal = new Rect(0, arrangedHeight, width, childHeight);
- child.Arrange(childFinal);
+ ArrangeChild(child, childFinal, finalSize, orientation);
arrangedWidth = Math.Max(arrangedWidth, childWidth);
arrangedHeight += childHeight + gap;
}
@@ -211,13 +227,13 @@ namespace Avalonia.Controls
{
double height = Math.Max(childHeight, arrangedHeight);
Rect childFinal = new Rect(arrangedWidth, 0, childWidth, height);
- child.Arrange(childFinal);
+ ArrangeChild(child, childFinal, finalSize, orientation);
arrangedWidth += childWidth + gap;
arrangedHeight = Math.Max(arrangedHeight, childHeight);
}
}
- if (Orientation == Orientation.Vertical)
+ if (orientation == Orientation.Vertical)
{
arrangedHeight = Math.Max(arrangedHeight - gap, finalSize.Height);
}
@@ -228,5 +244,14 @@ namespace Avalonia.Controls
return new Size(arrangedWidth, arrangedHeight);
}
+
+ internal virtual void ArrangeChild(
+ IControl child,
+ Rect rect,
+ Size panelSize,
+ Orientation orientation)
+ {
+ child.Arrange(rect);
+ }
}
}
\ No newline at end of file
diff --git a/src/Avalonia.Controls/Templates/DataTemplateExtensions.cs b/src/Avalonia.Controls/Templates/DataTemplateExtensions.cs
index 9eff4243b1..df25733524 100644
--- a/src/Avalonia.Controls/Templates/DataTemplateExtensions.cs
+++ b/src/Avalonia.Controls/Templates/DataTemplateExtensions.cs
@@ -11,54 +11,6 @@ namespace Avalonia.Controls.Templates
///
public static class DataTemplateExtensions
{
- ///
- /// Materializes a piece of data based on a data template.
- ///
- /// The control materializing the data template.
- /// The data.
- ///
- /// An optional primary template that can will be tried before the
- /// in the tree are searched.
- ///
- /// The data materialized as a control.
- public static IControl MaterializeDataTemplate(
- this IControl control,
- object data,
- IDataTemplate primary = null)
- {
- if (data == null)
- {
- return null;
- }
- else
- {
- var asControl = data as IControl;
-
- if (asControl != null)
- {
- return asControl;
- }
- else
- {
- IDataTemplate template = control.FindDataTemplate(data, primary);
- IControl result;
-
- if (template != null)
- {
- result = template.Build(data);
- }
- else
- {
- result = FuncDataTemplate.Default.Build(data);
- }
-
- NameScope.SetNameScope((Control)result, new NameScope());
-
- return result;
- }
- }
- }
-
///
/// Find a data template that matches a piece of data.
///
diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate.cs
index 8ce3f69f36..1d90fcd01e 100644
--- a/src/Avalonia.Controls/Templates/FuncDataTemplate.cs
+++ b/src/Avalonia.Controls/Templates/FuncDataTemplate.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
+using System.Reactive.Linq;
using System.Reflection;
namespace Avalonia.Controls.Templates
@@ -12,10 +13,26 @@ namespace Avalonia.Controls.Templates
public class FuncDataTemplate : FuncTemplate, IDataTemplate
{
///
- /// The default data template used in the case where not matching data template is found.
+ /// The default data template used in the case where no matching data template is found.
///
public static readonly FuncDataTemplate Default =
- new FuncDataTemplate(typeof(object), o => (o != null) ? new TextBlock { Text = o.ToString() } : null);
+ new FuncDataTemplate(
+ data =>
+ {
+ if (data != null)
+ {
+ var result = new TextBlock();
+ result.Bind(
+ TextBlock.TextProperty,
+ result.GetObservable(Control.DataContextProperty).Select(x => x?.ToString()));
+ return result;
+ }
+ else
+ {
+ return null;
+ }
+ },
+ true);
///
/// The implementation of the method.
@@ -29,8 +46,12 @@ namespace Avalonia.Controls.Templates
///
/// A function which when passed an object of returns a control.
///
- public FuncDataTemplate(Type type, Func build)
- : this(o => IsInstance(o, type), build)
+ /// Whether the control can be recycled.
+ public FuncDataTemplate(
+ Type type,
+ Func build,
+ bool supportsRecycling = false)
+ : this(o => IsInstance(o, type), build, supportsRecycling)
{
}
@@ -43,14 +64,22 @@ namespace Avalonia.Controls.Templates
///
/// A function which returns a control for matching data.
///
- public FuncDataTemplate(Func match, Func build)
+ /// Whether the control can be recycled.
+ public FuncDataTemplate(
+ Func match,
+ Func build,
+ bool supportsRecycling = false)
: base(build)
{
Contract.Requires(match != null);
_match = match;
+ SupportsRecycling = supportsRecycling;
}
+ ///
+ public bool SupportsRecycling { get; }
+
///
/// Checks to see if this data template matches the specified data.
///
diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs
index cb2d064605..7154c0e558 100644
--- a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs
+++ b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs
@@ -17,8 +17,9 @@ namespace Avalonia.Controls.Templates
///
/// A function which when passed an object of returns a control.
///
- public FuncDataTemplate(Func build)
- : base(typeof(T), CastBuild(build))
+ /// Whether the control can be recycled.
+ public FuncDataTemplate(Func build, bool supportsRecycling = false)
+ : base(typeof(T), CastBuild(build), supportsRecycling)
{
}
@@ -31,8 +32,12 @@ namespace Avalonia.Controls.Templates
///
/// A function which when passed an object of returns a control.
///
- public FuncDataTemplate(Func match, Func build)
- : base(CastMatch(match), CastBuild(build))
+ /// Whether the control can be recycled.
+ public FuncDataTemplate(
+ Func match,
+ Func build,
+ bool supportsRecycling = false)
+ : base(CastMatch(match), CastBuild(build), supportsRecycling)
{
}
diff --git a/src/Avalonia.Controls/Templates/FuncTemplate`1.cs b/src/Avalonia.Controls/Templates/FuncTemplate`1.cs
index c04b08de4d..88321ee1a3 100644
--- a/src/Avalonia.Controls/Templates/FuncTemplate`1.cs
+++ b/src/Avalonia.Controls/Templates/FuncTemplate`1.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
+using Avalonia.Styling;
namespace Avalonia.Controls.Templates
{
@@ -34,5 +35,7 @@ namespace Avalonia.Controls.Templates
{
return _func();
}
+
+ object ITemplate.Build() => Build();
}
}
\ No newline at end of file
diff --git a/src/Avalonia.Controls/Templates/IDataTemplate.cs b/src/Avalonia.Controls/Templates/IDataTemplate.cs
index ee490da4de..f9c97e55f3 100644
--- a/src/Avalonia.Controls/Templates/IDataTemplate.cs
+++ b/src/Avalonia.Controls/Templates/IDataTemplate.cs
@@ -8,6 +8,12 @@ namespace Avalonia.Controls.Templates
///
public interface IDataTemplate : ITemplate
{
+ ///
+ /// Gets a value indicating whether the data template supports recycling of the generated
+ /// control.
+ ///
+ bool SupportsRecycling { get; }
+
///
/// Checks to see if this data template matches the specified data.
///
diff --git a/src/Avalonia.Controls/Templates/ITemplate`1.cs b/src/Avalonia.Controls/Templates/ITemplate`1.cs
index 79c6ae47ba..2bf4b5a285 100644
--- a/src/Avalonia.Controls/Templates/ITemplate`1.cs
+++ b/src/Avalonia.Controls/Templates/ITemplate`1.cs
@@ -1,13 +1,15 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using Avalonia.Styling;
+
namespace Avalonia.Controls
{
///
/// Creates a control.
///
/// The type of control.
- public interface ITemplate where TControl : IControl
+ public interface ITemplate : ITemplate where TControl : IControl
{
///
/// Creates the control.
@@ -15,6 +17,6 @@ namespace Avalonia.Controls
///
/// The created control.
///
- TControl Build();
+ new TControl Build();
}
}
\ No newline at end of file
diff --git a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs
index 1e05c23e5a..ad95b0269a 100644
--- a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs
+++ b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs
@@ -17,17 +17,22 @@ namespace Avalonia.Controls.Utils
public static int Count(this IEnumerable items)
{
- Contract.Requires(items != null);
-
- var collection = items as ICollection;
-
- if (collection != null)
+ if (items != null)
{
- return collection.Count;
+ var collection = items as ICollection;
+
+ if (collection != null)
+ {
+ return collection.Count;
+ }
+ else
+ {
+ return Enumerable.Count(items.Cast());
+ }
}
else
{
- return Enumerable.Count(items.Cast());
+ return 0;
}
}
diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs
new file mode 100644
index 0000000000..46a17d601f
--- /dev/null
+++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs
@@ -0,0 +1,229 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections.Specialized;
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.Layout;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls
+{
+ public class VirtualizingStackPanel : StackPanel, IVirtualizingPanel
+ {
+ private Size _availableSpace;
+ private double _takenSpace;
+ private int _canBeRemoved;
+ private double _averageItemSize;
+ private int _averageCount;
+ private double _pixelOffset;
+
+ bool IVirtualizingPanel.IsFull
+ {
+ get
+ {
+ return Orientation == Orientation.Horizontal ?
+ _takenSpace >= _availableSpace.Width :
+ _takenSpace >= _availableSpace.Height;
+ }
+ }
+
+ IVirtualizingController IVirtualizingPanel.Controller { get; set; }
+ int IVirtualizingPanel.OverflowCount => _canBeRemoved;
+ Orientation IVirtualizingPanel.ScrollDirection => Orientation;
+ double IVirtualizingPanel.AverageItemSize => _averageItemSize;
+
+ double IVirtualizingPanel.PixelOverflow
+ {
+ get
+ {
+ var bounds = Orientation == Orientation.Horizontal ?
+ _availableSpace.Width : _availableSpace.Height;
+ return Math.Max(0, _takenSpace - bounds);
+ }
+ }
+
+ double IVirtualizingPanel.PixelOffset
+ {
+ get { return _pixelOffset; }
+
+ set
+ {
+ if (_pixelOffset != value)
+ {
+ _pixelOffset = value;
+ InvalidateArrange();
+ }
+ }
+ }
+
+ private IVirtualizingController Controller => ((IVirtualizingPanel)this).Controller;
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ if (availableSize != ((ILayoutable)this).PreviousMeasure)
+ {
+ // TODO: We need to put a reasonable limit on this, probably based on the max
+ // window size.
+ _availableSpace = availableSize;
+ Controller?.UpdateControls();
+ }
+
+ return base.MeasureOverride(availableSize);
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ _availableSpace = finalSize;
+ _canBeRemoved = 0;
+ _takenSpace = 0;
+ _averageItemSize = 0;
+ _averageCount = 0;
+ var result = base.ArrangeOverride(finalSize);
+ _takenSpace += _pixelOffset;
+ Controller?.UpdateControls();
+ return result;
+ }
+
+ protected override void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ base.ChildrenChanged(sender, e);
+
+ switch (e.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ foreach (IControl control in e.NewItems)
+ {
+ UpdateAdd(control);
+ }
+
+ break;
+
+ case NotifyCollectionChangedAction.Remove:
+ foreach (IControl control in e.OldItems)
+ {
+ UpdateRemove(control);
+ }
+
+ break;
+ }
+ }
+
+ protected override IInputElement GetControlInDirection(NavigationDirection direction, IControl from)
+ {
+ var logicalScrollable = Parent as ILogicalScrollable;
+ var fromControl = from as IControl;
+
+ if (logicalScrollable?.IsLogicalScrollEnabled == true && fromControl != null)
+ {
+ return logicalScrollable.GetControlInDirection(direction, fromControl);
+ }
+ else
+ {
+ return base.GetControlInDirection(direction, from);
+ }
+ }
+
+ internal override void ArrangeChild(
+ IControl child,
+ Rect rect,
+ Size panelSize,
+ Orientation orientation)
+ {
+ if (orientation == Orientation.Vertical)
+ {
+ rect = new Rect(rect.X, rect.Y - _pixelOffset, rect.Width, rect.Height);
+ child.Arrange(rect);
+
+ if (rect.Y >= _availableSpace.Height)
+ {
+ ++_canBeRemoved;
+ }
+
+ if (rect.Bottom >= _takenSpace)
+ {
+ _takenSpace = rect.Bottom;
+ }
+
+ AddToAverageItemSize(rect.Height);
+ }
+ else
+ {
+ rect = new Rect(rect.X - _pixelOffset, rect.Y, rect.Width, rect.Height);
+ child.Arrange(rect);
+
+ if (rect.X >= _availableSpace.Width)
+ {
+ ++_canBeRemoved;
+ }
+
+ if (rect.Right >= _takenSpace)
+ {
+ _takenSpace = rect.Right;
+ }
+
+ AddToAverageItemSize(rect.Width);
+ }
+ }
+
+ private void UpdateAdd(IControl child)
+ {
+ var bounds = Bounds;
+ var gap = Gap;
+
+ child.Measure(_availableSpace);
+ ++_averageCount;
+
+ if (Orientation == Orientation.Vertical)
+ {
+ var height = child.DesiredSize.Height;
+ _takenSpace += height + gap;
+ AddToAverageItemSize(height);
+ }
+ else
+ {
+ var width = child.DesiredSize.Width;
+ _takenSpace += width + gap;
+ AddToAverageItemSize(width);
+ }
+ }
+
+ private void UpdateRemove(IControl child)
+ {
+ var bounds = Bounds;
+ var gap = Gap;
+
+ if (Orientation == Orientation.Vertical)
+ {
+ var height = child.DesiredSize.Height;
+ _takenSpace -= height + gap;
+ RemoveFromAverageItemSize(height);
+ }
+ else
+ {
+ var width = child.DesiredSize.Width;
+ _takenSpace -= width + gap;
+ RemoveFromAverageItemSize(width);
+ }
+
+ if (_canBeRemoved > 0)
+ {
+ --_canBeRemoved;
+ }
+ }
+
+ private void AddToAverageItemSize(double value)
+ {
+ ++_averageCount;
+ _averageItemSize += (value - _averageItemSize) / _averageCount;
+ }
+
+ private void RemoveFromAverageItemSize(double value)
+ {
+ _averageItemSize = ((_averageItemSize * _averageCount) - value) / (_averageCount - 1);
+ --_averageCount;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs
index 1000e3ef20..36bc87af02 100644
--- a/src/Avalonia.Controls/Window.cs
+++ b/src/Avalonia.Controls/Window.cs
@@ -89,7 +89,16 @@ namespace Avalonia.Controls
/// Initializes a new instance of the class.
///
public Window()
- : base(PlatformManager.CreateWindow())
+ : this(PlatformManager.CreateWindow())
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The window implementation.
+ public Window(IWindowImpl impl)
+ : base(impl)
{
_maxPlatformClientSize = this.PlatformImpl.MaxClientSize;
}
diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs
index d0f1e93c56..9d3a5b6536 100644
--- a/src/Avalonia.Controls/WrapPanel.cs
+++ b/src/Avalonia.Controls/WrapPanel.cs
@@ -48,35 +48,35 @@ namespace Avalonia.Controls
/// The movement direction.
/// The control from which movement begins.
/// The control.
- IInputElement INavigableContainer.GetControl(FocusNavigationDirection direction, IInputElement from)
+ IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from)
{
var horiz = Orientation == Orientation.Horizontal;
int index = Children.IndexOf((IControl)from);
switch (direction)
{
- case FocusNavigationDirection.First:
+ case NavigationDirection.First:
index = 0;
break;
- case FocusNavigationDirection.Last:
+ case NavigationDirection.Last:
index = Children.Count - 1;
break;
- case FocusNavigationDirection.Next:
+ case NavigationDirection.Next:
++index;
break;
- case FocusNavigationDirection.Previous:
+ case NavigationDirection.Previous:
--index;
break;
- case FocusNavigationDirection.Left:
+ case NavigationDirection.Left:
index = horiz ? index - 1 : -1;
break;
- case FocusNavigationDirection.Right:
+ case NavigationDirection.Right:
index = horiz ? index + 1 : -1;
break;
- case FocusNavigationDirection.Up:
+ case NavigationDirection.Up:
index = horiz ? -1 : index - 1;
break;
- case FocusNavigationDirection.Down:
+ case NavigationDirection.Down:
index = horiz ? -1 : index + 1;
break;
}
diff --git a/src/Avalonia.DesignerSupport/DesignerApi.cs b/src/Avalonia.DesignerSupport/DesignerApi.cs
index 7c17051ce2..cbbfbffec8 100644
--- a/src/Avalonia.DesignerSupport/DesignerApi.cs
+++ b/src/Avalonia.DesignerSupport/DesignerApi.cs
@@ -7,33 +7,42 @@ using System.Threading.Tasks;
namespace Avalonia.DesignerSupport
{
- class DesignerApi
+ class DesignerApiDictionary
{
- private readonly Dictionary _inner;
+ public Dictionary Dictionary { get; set; }
- public DesignerApi(Dictionary inner)
+ public DesignerApiDictionary(Dictionary dictionary)
{
- _inner = inner;
+ Dictionary = dictionary;
}
- object Get([CallerMemberName] string name = null)
+ protected object Get([CallerMemberName] string name = null)
{
object rv;
- _inner.TryGetValue(name, out rv);
+ Dictionary.TryGetValue(name, out rv);
return rv;
}
- void Set(object value, [CallerMemberName] string name = null)
+ protected void Set(object value, [CallerMemberName] string name = null)
{
- _inner[name] = value;
+ Dictionary[name] = value;
}
+ }
+ class DesignerApi : DesignerApiDictionary
+ {
public Action UpdateXaml
{
get { return (Action) Get(); }
set {Set(value); }
}
+ public Action> UpdateXaml2
+ {
+ get { return (Action>)Get(); }
+ set { Set(value); }
+ }
+
public Action OnResize
{
get { return (Action) Get(); }
@@ -52,5 +61,32 @@ namespace Avalonia.DesignerSupport
get { return (Action) Get(); }
}
+ public DesignerApi(Dictionary dictionary) : base(dictionary)
+ {
+ }
+ }
+
+ class DesignerApiXamlFileInfo : DesignerApiDictionary
+ {
+ public string Xaml
+ {
+ get { return (string)Get(); }
+ set { Set(value); }
+ }
+
+ public string AssemblyPath
+ {
+ get { return (string) Get(); }
+ set { Set(value); }
+ }
+
+ public DesignerApiXamlFileInfo(Dictionary dictionary) : base(dictionary)
+ {
+ }
+
+ public DesignerApiXamlFileInfo(): base(new Dictionary())
+ {
+
+ }
}
}
diff --git a/src/Avalonia.DesignerSupport/DesignerAssist.cs b/src/Avalonia.DesignerSupport/DesignerAssist.cs
index 64aa9106c7..8787144665 100644
--- a/src/Avalonia.DesignerSupport/DesignerAssist.cs
+++ b/src/Avalonia.DesignerSupport/DesignerAssist.cs
@@ -36,7 +36,7 @@ namespace Avalonia.DesignerSupport
public static void Init(Dictionary shared)
{
Design.IsDesignMode = true;
- Api = new DesignerApi(shared) {UpdateXaml = UpdateXaml, SetScalingFactor = SetScalingFactor};
+ Api = new DesignerApi(shared) {UpdateXaml = UpdateXaml, UpdateXaml2 = UpdateXaml2, SetScalingFactor = SetScalingFactor};
var plat = (IPclPlatformWrapper) Activator.CreateInstance(Assembly.Load(new AssemblyName("Avalonia.Win32"))
.DefinedTypes.First(typeof (IPclPlatformWrapper).GetTypeInfo().IsAssignableFrom).AsType());
@@ -58,10 +58,9 @@ namespace Avalonia.DesignerSupport
//Ignore, Assembly.DefinedTypes threw an exception, we can't do anything about that
}
}
-
AppBuilder.Configure(app == null ? new DesignerApp() : (Application) Activator.CreateInstance(app.AsType()))
- .WithWindowingSubsystem(Application.InitializeWin32Subsystem)
- .WithRenderingSubsystem(() => { })
+ .UseWindowingSubsystem("Avalonia.Win32")
+ .UseRenderingSubsystem("Avalonia.Direct2D1")
.SetupWithoutStarting();
}
@@ -74,22 +73,65 @@ namespace Avalonia.DesignerSupport
static Window s_currentWindow;
- private static void UpdateXaml(string xaml)
+ private static void UpdateXaml(string xaml) => UpdateXaml2(new DesignerApiXamlFileInfo
+ {
+ Xaml = xaml
+ }.Dictionary);
+
+ private static void UpdateXaml2(Dictionary dic)
{
+ var xamlInfo = new DesignerApiXamlFileInfo(dic);
Window window;
- Control original;
+ Control control;
using (PlatformManager.DesignerMode())
{
var loader = new AvaloniaXamlLoader();
- var stream = new MemoryStream(Encoding.UTF8.GetBytes(xaml));
+ var stream = new MemoryStream(Encoding.UTF8.GetBytes(xamlInfo.Xaml));
- original = (Control)loader.Load(stream);
- window = original as Window;
+
+ Uri baseUri = null;
+ if (xamlInfo.AssemblyPath != null)
+ {
+ //Fabricate fake Uri
+ baseUri =
+ new Uri("resm:Fake.xaml?assembly=" + Path.GetFileNameWithoutExtension(xamlInfo.AssemblyPath));
+ }
+
+ var loaded = loader.Load(stream, null, baseUri);
+ var styles = loaded as Styles;
+ if (styles != null)
+ {
+ var substitute = Design.GetPreviewWith(styles) ??
+ styles.Select(Design.GetPreviewWith).FirstOrDefault(s => s != null);
+ if (substitute != null)
+ {
+ substitute.Styles.AddRange(styles);
+ control = substitute;
+ }
+ else
+ control = new StackPanel
+ {
+ Children =
+ {
+ new TextBlock {Text = "Styles can't be previewed without Design.PreviewWith. Add"},
+ new TextBlock {Text = ""},
+ new TextBlock {Text = " "},
+ new TextBlock {Text = ""},
+ new TextBlock {Text = "before setters in your first Style"}
+ }
+ };
+ }
+ if (loaded is Application)
+ control = new TextBlock {Text = "Application can't be previewed in design view"};
+ else
+ control = (Control) loaded;
+
+ window = control as Window;
if (window == null)
{
- window = new Window() {Content = original};
+ window = new Window() {Content = (Control)control};
}
if (!window.IsSet(Window.SizeToContentProperty))
@@ -99,7 +141,7 @@ namespace Avalonia.DesignerSupport
s_currentWindow?.Close();
s_currentWindow = window;
window.Show();
- Design.ApplyDesignerProperties(window, original);
+ Design.ApplyDesignerProperties(window, control);
Api.OnWindowCreated?.Invoke(window.PlatformImpl.Handle.Handle);
Api.OnResize?.Invoke();
}
diff --git a/src/Avalonia.Diagnostics/ViewLocator.cs b/src/Avalonia.Diagnostics/ViewLocator.cs
index ddaa11b092..b107338aec 100644
--- a/src/Avalonia.Diagnostics/ViewLocator.cs
+++ b/src/Avalonia.Diagnostics/ViewLocator.cs
@@ -9,6 +9,8 @@ namespace Avalonia.Diagnostics
{
public class ViewLocator : IDataTemplate
{
+ public bool SupportsRecycling => false;
+
public IControl Build(object data)
{
var name = data.GetType().FullName.Replace("ViewModel", "View");
diff --git a/src/Avalonia.Input/Avalonia.Input.csproj b/src/Avalonia.Input/Avalonia.Input.csproj
index 569e1bb2cc..3722c80def 100644
--- a/src/Avalonia.Input/Avalonia.Input.csproj
+++ b/src/Avalonia.Input/Avalonia.Input.csproj
@@ -1,4 +1,4 @@
-
+
@@ -79,7 +79,7 @@
-
+
diff --git a/src/Avalonia.Input/IKeyboardNavigationHandler.cs b/src/Avalonia.Input/IKeyboardNavigationHandler.cs
index 14e2144da9..db3a3cf114 100644
--- a/src/Avalonia.Input/IKeyboardNavigationHandler.cs
+++ b/src/Avalonia.Input/IKeyboardNavigationHandler.cs
@@ -25,7 +25,7 @@ namespace Avalonia.Input
/// Any input modifiers active at the time of focus.
void Move(
IInputElement element,
- FocusNavigationDirection direction,
+ NavigationDirection direction,
InputModifiers modifiers = InputModifiers.None);
}
}
\ No newline at end of file
diff --git a/src/Avalonia.Input/INavigableContainer.cs b/src/Avalonia.Input/INavigableContainer.cs
index 48f85819c0..13d734bd0b 100644
--- a/src/Avalonia.Input/INavigableContainer.cs
+++ b/src/Avalonia.Input/INavigableContainer.cs
@@ -14,6 +14,6 @@ namespace Avalonia.Input
/// The movement direction.
/// The control from which movement begins.
/// The control.
- IInputElement GetControl(FocusNavigationDirection direction, IInputElement from);
+ IInputElement GetControl(NavigationDirection direction, IInputElement from);
}
}
diff --git a/src/Avalonia.Input/InputExtensions.cs b/src/Avalonia.Input/InputExtensions.cs
index a2321f3493..35ffeca7bb 100644
--- a/src/Avalonia.Input/InputExtensions.cs
+++ b/src/Avalonia.Input/InputExtensions.cs
@@ -24,14 +24,14 @@ namespace Avalonia.Input
public static IEnumerable GetInputElementsAt(this IInputElement element, Point p)
{
Contract.Requires(element != null);
- var transformedBounds = BoundsTracker.GetTransformedBounds((Visual)element);
- var geometry = transformedBounds.GetTransformedBoundsGeometry();
if (element.IsVisible &&
element.IsHitTestVisible &&
element.IsEnabledCore)
{
- if (element.VisualChildren.Any())
+ bool containsPoint = BoundsTracker.GetTransformedBounds((Visual)element).Contains(p);
+
+ if ((containsPoint || !element.ClipToBounds) && element.VisualChildren.Any())
{
foreach (var child in ZSort(element.VisualChildren.OfType()))
{
@@ -42,7 +42,7 @@ namespace Avalonia.Input
}
}
- if (geometry.FillContains(p))
+ if (containsPoint)
{
yield return element;
}
@@ -71,7 +71,6 @@ namespace Avalonia.Input
})
.OrderBy(x => x, null)
.Select(x => x.Element);
-
}
private class ZOrderElement : IComparable
@@ -95,4 +94,4 @@ namespace Avalonia.Input
}
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs
index 9991f7ec10..57da49fa03 100644
--- a/src/Avalonia.Input/KeyboardNavigationHandler.cs
+++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs
@@ -48,11 +48,11 @@ namespace Avalonia.Input
///
public static IInputElement GetNext(
IInputElement element,
- FocusNavigationDirection direction)
+ NavigationDirection direction)
{
Contract.Requires(element != null);
- if (direction == FocusNavigationDirection.Next || direction == FocusNavigationDirection.Previous)
+ if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous)
{
return TabNavigation.GetNextInTabOrder(element, direction);
}
@@ -70,7 +70,7 @@ namespace Avalonia.Input
/// Any input modifiers active at the time of focus.
public void Move(
IInputElement element,
- FocusNavigationDirection direction,
+ NavigationDirection direction,
InputModifiers modifiers = InputModifiers.None)
{
Contract.Requires(element != null);
@@ -79,8 +79,8 @@ namespace Avalonia.Input
if (next != null)
{
- var method = direction == FocusNavigationDirection.Next ||
- direction == FocusNavigationDirection.Previous ?
+ var method = direction == NavigationDirection.Next ||
+ direction == NavigationDirection.Previous ?
NavigationMethod.Tab : NavigationMethod.Directional;
FocusManager.Instance.Focus(next, method, modifiers);
}
@@ -97,25 +97,37 @@ namespace Avalonia.Input
if (current != null)
{
- FocusNavigationDirection? direction = null;
+ NavigationDirection? direction = null;
switch (e.Key)
{
case Key.Tab:
direction = (e.Modifiers & InputModifiers.Shift) == 0 ?
- FocusNavigationDirection.Next : FocusNavigationDirection.Previous;
+ NavigationDirection.Next : NavigationDirection.Previous;
break;
case Key.Up:
- direction = FocusNavigationDirection.Up;
+ direction = NavigationDirection.Up;
break;
case Key.Down:
- direction = FocusNavigationDirection.Down;
+ direction = NavigationDirection.Down;
break;
case Key.Left:
- direction = FocusNavigationDirection.Left;
+ direction = NavigationDirection.Left;
break;
case Key.Right:
- direction = FocusNavigationDirection.Right;
+ direction = NavigationDirection.Right;
+ break;
+ case Key.PageUp:
+ direction = NavigationDirection.PageUp;
+ break;
+ case Key.PageDown:
+ direction = NavigationDirection.PageDown;
+ break;
+ case Key.Home:
+ direction = NavigationDirection.First;
+ break;
+ case Key.End:
+ direction = NavigationDirection.Last;
break;
}
diff --git a/src/Avalonia.Input/Navigation/DirectionalNavigation.cs b/src/Avalonia.Input/Navigation/DirectionalNavigation.cs
index 4d6e706516..6d4da5d976 100644
--- a/src/Avalonia.Input/Navigation/DirectionalNavigation.cs
+++ b/src/Avalonia.Input/Navigation/DirectionalNavigation.cs
@@ -24,18 +24,17 @@ namespace Avalonia.Input.Navigation
///
public static IInputElement GetNext(
IInputElement element,
- FocusNavigationDirection direction)
+ NavigationDirection direction)
{
Contract.Requires(element != null);
Contract.Requires(
- direction != FocusNavigationDirection.Next &&
- direction != FocusNavigationDirection.Previous);
+ direction != NavigationDirection.Next &&
+ direction != NavigationDirection.Previous);
var container = element.GetVisualParent();
if (container != null)
{
- var isForward = IsForward(direction);
var mode = KeyboardNavigation.GetDirectionalNavigation((InputElement)container);
switch (mode)
@@ -63,12 +62,12 @@ namespace Avalonia.Input.Navigation
///
/// The direction.
/// True if the direction is forward.
- private static bool IsForward(FocusNavigationDirection direction)
+ private static bool IsForward(NavigationDirection direction)
{
- return direction == FocusNavigationDirection.Next ||
- direction == FocusNavigationDirection.Last ||
- direction == FocusNavigationDirection.Right ||
- direction == FocusNavigationDirection.Down;
+ return direction == NavigationDirection.Next ||
+ direction == NavigationDirection.Last ||
+ direction == NavigationDirection.Right ||
+ direction == NavigationDirection.Down;
}
///
@@ -77,7 +76,7 @@ namespace Avalonia.Input.Navigation
/// The element.
/// The direction to search.
/// The element or null if not found.##
- private static IInputElement GetFocusableDescendent(IInputElement container, FocusNavigationDirection direction)
+ private static IInputElement GetFocusableDescendent(IInputElement container, NavigationDirection direction)
{
return IsForward(direction) ?
GetFocusableDescendents(container).FirstOrDefault() :
@@ -121,9 +120,9 @@ namespace Avalonia.Input.Navigation
private static IInputElement GetNextInContainer(
IInputElement element,
IInputElement container,
- FocusNavigationDirection direction)
+ NavigationDirection direction)
{
- if (direction == FocusNavigationDirection.Down)
+ if (direction == NavigationDirection.Down)
{
var descendent = GetFocusableDescendents(element).FirstOrDefault();
@@ -156,7 +155,7 @@ namespace Avalonia.Input.Navigation
element = null;
}
- if (element != null && direction == FocusNavigationDirection.Up)
+ if (element != null && direction == NavigationDirection.Up)
{
var descendent = GetFocusableDescendents(element).LastOrDefault();
@@ -180,7 +179,7 @@ namespace Avalonia.Input.Navigation
/// The first element, or null if there are no more elements.
private static IInputElement GetFirstInNextContainer(
IInputElement container,
- FocusNavigationDirection direction)
+ NavigationDirection direction)
{
var parent = container.GetVisualParent();
var isForward = IsForward(direction);
diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs
index 90502cdbf9..bc3826d90e 100644
--- a/src/Avalonia.Input/Navigation/TabNavigation.cs
+++ b/src/Avalonia.Input/Navigation/TabNavigation.cs
@@ -24,12 +24,12 @@ namespace Avalonia.Input.Navigation
///
public static IInputElement GetNextInTabOrder(
IInputElement element,
- FocusNavigationDirection direction)
+ NavigationDirection direction)
{
Contract.Requires(element != null);
Contract.Requires(
- direction == FocusNavigationDirection.Next ||
- direction == FocusNavigationDirection.Previous);
+ direction == NavigationDirection.Next ||
+ direction == NavigationDirection.Previous);
var container = element.GetVisualParent();
@@ -63,9 +63,9 @@ namespace Avalonia.Input.Navigation
/// The element.
/// The direction to search.
/// The element or null if not found.##
- private static IInputElement GetFocusableDescendent(IInputElement container, FocusNavigationDirection direction)
+ private static IInputElement GetFocusableDescendent(IInputElement container, NavigationDirection direction)
{
- return direction == FocusNavigationDirection.Next ?
+ return direction == NavigationDirection.Next ?
GetFocusableDescendents(container).FirstOrDefault() :
GetFocusableDescendents(container).LastOrDefault();
}
@@ -128,9 +128,9 @@ namespace Avalonia.Input.Navigation
private static IInputElement GetNextInContainer(
IInputElement element,
IInputElement container,
- FocusNavigationDirection direction)
+ NavigationDirection direction)
{
- if (direction == FocusNavigationDirection.Next)
+ if (direction == NavigationDirection.Next)
{
var descendent = GetFocusableDescendents(element).FirstOrDefault();
@@ -165,7 +165,7 @@ namespace Avalonia.Input.Navigation
element = null;
}
- if (element != null && direction == FocusNavigationDirection.Previous)
+ if (element != null && direction == NavigationDirection.Previous)
{
var descendent = GetFocusableDescendents(element).LastOrDefault();
@@ -189,14 +189,14 @@ namespace Avalonia.Input.Navigation
/// The first element, or null if there are no more elements.
private static IInputElement GetFirstInNextContainer(
IInputElement container,
- FocusNavigationDirection direction)
+ NavigationDirection direction)
{
var parent = container.GetVisualParent();
IInputElement next = null;
if (parent != null)
{
- if (direction == FocusNavigationDirection.Previous && parent.CanFocus())
+ if (direction == NavigationDirection.Previous && parent.CanFocus())
{
return parent;
}
@@ -204,7 +204,7 @@ namespace Avalonia.Input.Navigation
var siblings = parent.GetVisualChildren()
.OfType()
.Where(FocusExtensions.CanFocusDescendents);
- var sibling = direction == FocusNavigationDirection.Next ?
+ var sibling = direction == NavigationDirection.Next ?
siblings.SkipWhile(x => x != container).Skip(1).FirstOrDefault() :
siblings.TakeWhile(x => x != container).LastOrDefault();
@@ -216,7 +216,7 @@ namespace Avalonia.Input.Navigation
}
else
{
- next = direction == FocusNavigationDirection.Next ?
+ next = direction == NavigationDirection.Next ?
GetFocusableDescendents(sibling).FirstOrDefault() :
GetFocusableDescendents(sibling).LastOrDefault();
}
@@ -229,7 +229,7 @@ namespace Avalonia.Input.Navigation
}
else
{
- next = direction == FocusNavigationDirection.Next ?
+ next = direction == NavigationDirection.Next ?
GetFocusableDescendents(container).FirstOrDefault() :
GetFocusableDescendents(container).LastOrDefault();
}
diff --git a/src/Avalonia.Input/FocusNavigationDirection.cs b/src/Avalonia.Input/NavigationDirection.cs
similarity index 78%
rename from src/Avalonia.Input/FocusNavigationDirection.cs
rename to src/Avalonia.Input/NavigationDirection.cs
index c8f670a13c..fbaa7e74c7 100644
--- a/src/Avalonia.Input/FocusNavigationDirection.cs
+++ b/src/Avalonia.Input/NavigationDirection.cs
@@ -4,9 +4,9 @@
namespace Avalonia.Input
{
///
- /// Describes how focus should be moved.
+ /// Describes how focus should be moved by directional or tab keys.
///
- public enum FocusNavigationDirection
+ public enum NavigationDirection
{
///
/// Move the focus to the next control in the tab order.
@@ -47,5 +47,15 @@ namespace Avalonia.Input
/// Move the focus down.
///
Down,
+
+ ///
+ /// Move the focus up a page.
+ ///
+ PageUp,
+
+ ///
+ /// Move the focus down a page.
+ ///
+ PageDown,
}
}
diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs
index 42ed45b0bb..85daff28b9 100644
--- a/src/Avalonia.Layout/LayoutManager.cs
+++ b/src/Avalonia.Layout/LayoutManager.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using Avalonia.Logging;
using Avalonia.Threading;
@@ -13,8 +14,8 @@ namespace Avalonia.Layout
///
public class LayoutManager : ILayoutManager
{
- private readonly Queue _toMeasure = new Queue();
- private readonly Queue _toArrange = new Queue();
+ private readonly HashSet _toMeasure = new HashSet();
+ private readonly HashSet _toArrange = new HashSet();
private bool _queued;
private bool _running;
@@ -29,8 +30,8 @@ namespace Avalonia.Layout
Contract.Requires(control != null);
Dispatcher.UIThread.VerifyAccess();
- _toMeasure.Enqueue(control);
- _toArrange.Enqueue(control);
+ _toMeasure.Add(control);
+ _toArrange.Add(control);
QueueLayoutPass();
}
@@ -40,7 +41,7 @@ namespace Avalonia.Layout
Contract.Requires(control != null);
Dispatcher.UIThread.VerifyAccess();
- _toArrange.Enqueue(control);
+ _toArrange.Add(control);
QueueLayoutPass();
}
@@ -107,7 +108,7 @@ namespace Avalonia.Layout
{
while (_toMeasure.Count > 0)
{
- var next = _toMeasure.Dequeue();
+ var next = _toMeasure.First();
Measure(next);
}
}
@@ -116,7 +117,7 @@ namespace Avalonia.Layout
{
while (_toArrange.Count > 0 && _toMeasure.Count == 0)
{
- var next = _toArrange.Dequeue();
+ var next = _toArrange.First();
Arrange(next);
}
}
@@ -124,29 +125,45 @@ namespace Avalonia.Layout
private void Measure(ILayoutable control)
{
var root = control as ILayoutRoot;
+ var parent = control.VisualParent as ILayoutable;
if (root != null)
{
root.Measure(root.MaxClientSize);
}
- else if (control.PreviousMeasure.HasValue)
+ else if (parent != null)
+ {
+ Measure(parent);
+ }
+
+ if (!control.IsMeasureValid)
{
control.Measure(control.PreviousMeasure.Value);
}
+
+ _toMeasure.Remove(control);
}
private void Arrange(ILayoutable control)
{
var root = control as ILayoutRoot;
+ var parent = control.VisualParent as ILayoutable;
if (root != null)
{
root.Arrange(new Rect(root.DesiredSize));
}
- else if (control.PreviousArrange.HasValue)
+ else if (parent != null)
+ {
+ Arrange(parent);
+ }
+
+ if (control.PreviousArrange.HasValue)
{
control.Arrange(control.PreviousArrange.Value);
}
+
+ _toArrange.Remove(control);
}
private void QueueLayoutPass()
diff --git a/src/Avalonia.SceneGraph/Media/Imaging/Bitmap.cs b/src/Avalonia.SceneGraph/Media/Imaging/Bitmap.cs
index 5b84b0ddd3..3473c4a094 100644
--- a/src/Avalonia.SceneGraph/Media/Imaging/Bitmap.cs
+++ b/src/Avalonia.SceneGraph/Media/Imaging/Bitmap.cs
@@ -55,7 +55,8 @@ namespace Avalonia.Media.Imaging
///
public IBitmapImpl PlatformImpl
{
- get; }
+ get;
+ }
///
/// Saves the bitmap to a file.
diff --git a/src/Avalonia.SceneGraph/Rect.cs b/src/Avalonia.SceneGraph/Rect.cs
index de4d13890b..10253ec829 100644
--- a/src/Avalonia.SceneGraph/Rect.cs
+++ b/src/Avalonia.SceneGraph/Rect.cs
@@ -224,14 +224,24 @@ namespace Avalonia
}
///
- /// Determines whether a points in in the bounds of the rectangle.
+ /// Determines whether a point in in the bounds of the rectangle.
///
/// The point.
/// true if the point is in the bounds of the rectangle; otherwise false.
public bool Contains(Point p)
{
- return p.X >= _x && p.X < _x + _width &&
- p.Y >= _y && p.Y < _y + _height;
+ return p.X >= _x && p.X <= _x + _width &&
+ p.Y >= _y && p.Y <= _y + _height;
+ }
+
+ ///
+ /// Determines whether the rectangle fully contains another rectangle.
+ ///
+ /// The rectangle.
+ /// true if the rectangle is fully contained; otherwise false.
+ public bool Contains(Rect r)
+ {
+ return Contains(r.TopLeft) && Contains(r.BottomRight);
}
///
diff --git a/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs b/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs
index 688b0d9ead..0bb5bf63f8 100644
--- a/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs
+++ b/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs
@@ -104,7 +104,7 @@ namespace Avalonia.Rendering
if (visual.RenderTransform != null)
{
- var origin = visual.TransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height));
+ var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height));
var offset = Matrix.CreateTranslation(origin);
renderTransform = (-offset) * visual.RenderTransform.Value * (offset);
}
@@ -172,7 +172,7 @@ namespace Avalonia.Rendering
}
else
{
- var origin = visual.TransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height));
+ var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height));
var offset = Matrix.CreateTranslation(visual.Bounds.Position + origin);
var m = (-offset) * visual.RenderTransform.Value * (offset);
return visual.Bounds.TransformToAABB(m);
diff --git a/src/Avalonia.SceneGraph/Visual.cs b/src/Avalonia.SceneGraph/Visual.cs
index 82ce1b85c7..fb4cb21a7c 100644
--- a/src/Avalonia.SceneGraph/Visual.cs
+++ b/src/Avalonia.SceneGraph/Visual.cs
@@ -70,10 +70,10 @@ namespace Avalonia
AvaloniaProperty.Register(nameof(RenderTransform));
///
- /// Defines the property.
+ /// Defines the property.
///
- public static readonly StyledProperty TransformOriginProperty =
- AvaloniaProperty.Register(nameof(TransformOrigin), defaultValue: RelativePoint.Center);
+ public static readonly StyledProperty RenderTransformOriginProperty =
+ AvaloniaProperty.Register(nameof(RenderTransformOrigin), defaultValue: RelativePoint.Center);
///
/// Defines the property.
@@ -196,10 +196,10 @@ namespace Avalonia
///
/// Gets the transform origin of the scene graph node.
///
- public RelativePoint TransformOrigin
+ public RelativePoint RenderTransformOrigin
{
- get { return GetValue(TransformOriginProperty); }
- set { SetValue(TransformOriginProperty, value); }
+ get { return GetValue(RenderTransformOriginProperty); }
+ set { SetValue(RenderTransformOriginProperty, value); }
}
///
diff --git a/src/Avalonia.SceneGraph/VisualTree/IVisual.cs b/src/Avalonia.SceneGraph/VisualTree/IVisual.cs
index 9b4a4fccf5..b9b37b1a20 100644
--- a/src/Avalonia.SceneGraph/VisualTree/IVisual.cs
+++ b/src/Avalonia.SceneGraph/VisualTree/IVisual.cs
@@ -77,9 +77,9 @@ namespace Avalonia.VisualTree
Transform RenderTransform { get; set; }
///
- /// Gets or sets the transform origin of the scene graph node.
+ /// Gets or sets the render transform origin of the scene graph node.
///
- RelativePoint TransformOrigin { get; set; }
+ RelativePoint RenderTransformOrigin { get; set; }
///
/// Gets the scene graph node's child nodes.
diff --git a/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs b/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs
index da8ed01ccc..4c548669bd 100644
--- a/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs
+++ b/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs
@@ -38,20 +38,18 @@ namespace Avalonia.VisualTree
///
public Matrix Transform { get; }
- public Geometry GetTransformedBoundsGeometry()
+ public bool Contains(Point point)
{
- StreamGeometry geometry = new StreamGeometry();
- using (var context = geometry.Open())
+ if (Transform.HasInverse)
{
- context.SetFillRule(FillRule.EvenOdd);
- context.BeginFigure(Bounds.TopLeft * Transform, true);
- context.LineTo(Bounds.TopRight * Transform);
- context.LineTo(Bounds.BottomRight * Transform);
- context.LineTo(Bounds.BottomLeft * Transform);
- context.LineTo(Bounds.TopLeft * Transform);
- context.EndFigure(true);
+ Point trPoint = point * Transform.Invert();
+
+ return Bounds.Contains(trPoint);
+ }
+ else
+ {
+ return Bounds.Contains(point);
}
- return geometry;
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Avalonia.Styling/Avalonia.Styling.csproj b/src/Avalonia.Styling/Avalonia.Styling.csproj
index d40627a16e..5117aeb8c3 100644
--- a/src/Avalonia.Styling/Avalonia.Styling.csproj
+++ b/src/Avalonia.Styling/Avalonia.Styling.csproj
@@ -1,4 +1,4 @@
-
+
@@ -43,12 +43,17 @@
Properties\SharedAssemblyInfo.cs
+
+
+
+
+
@@ -92,6 +97,10 @@
+
+ {d211e587-d8bc-45b9-95a4-f297c8fa5200}
+ Avalonia.Animation
+
{B09B78D8-9B26-48B0-9149-D64A2F120F3F}
Avalonia.Base
diff --git a/src/Avalonia.Controls/INameScope.cs b/src/Avalonia.Styling/Controls/INameScope.cs
similarity index 100%
rename from src/Avalonia.Controls/INameScope.cs
rename to src/Avalonia.Styling/Controls/INameScope.cs
diff --git a/src/Avalonia.Controls/NameScope.cs b/src/Avalonia.Styling/Controls/NameScope.cs
similarity index 93%
rename from src/Avalonia.Controls/NameScope.cs
rename to src/Avalonia.Styling/Controls/NameScope.cs
index aeba4158ef..ddfb3ea173 100644
--- a/src/Avalonia.Controls/NameScope.cs
+++ b/src/Avalonia.Styling/Controls/NameScope.cs
@@ -15,7 +15,7 @@ namespace Avalonia.Controls
/// Defines the NameScope attached property.
///
public static readonly AttachedProperty NameScopeProperty =
- AvaloniaProperty.RegisterAttached("NameScope");
+ AvaloniaProperty.RegisterAttached("NameScope");
private readonly Dictionary _inner = new Dictionary();
@@ -34,7 +34,7 @@ namespace Avalonia.Controls
///
/// The visual.
/// The value of the NameScope attached property.
- public static INameScope GetNameScope(Control visual)
+ public static INameScope GetNameScope(Visual visual)
{
return visual.GetValue(NameScopeProperty);
}
@@ -44,7 +44,7 @@ namespace Avalonia.Controls
///
/// The visual.
/// The value to set.
- public static void SetNameScope(Control visual, INameScope value)
+ public static void SetNameScope(Visual visual, INameScope value)
{
visual.SetValue(NameScopeProperty, value);
}
diff --git a/src/Avalonia.Controls/NameScopeEventArgs.cs b/src/Avalonia.Styling/Controls/NameScopeEventArgs.cs
similarity index 100%
rename from src/Avalonia.Controls/NameScopeEventArgs.cs
rename to src/Avalonia.Styling/Controls/NameScopeEventArgs.cs
diff --git a/src/Avalonia.Controls/NameScopeExtensions.cs b/src/Avalonia.Styling/Controls/NameScopeExtensions.cs
similarity index 100%
rename from src/Avalonia.Controls/NameScopeExtensions.cs
rename to src/Avalonia.Styling/Controls/NameScopeExtensions.cs
diff --git a/src/Avalonia.Styling/Styling/ITemplate.cs b/src/Avalonia.Styling/Styling/ITemplate.cs
new file mode 100644
index 0000000000..1593326e37
--- /dev/null
+++ b/src/Avalonia.Styling/Styling/ITemplate.cs
@@ -0,0 +1,10 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+namespace Avalonia.Styling
+{
+ public interface ITemplate
+ {
+ object Build();
+ }
+}
diff --git a/src/Avalonia.Styling/Styling/Setter.cs b/src/Avalonia.Styling/Styling/Setter.cs
index 09faf37360..7f696a1f0a 100644
--- a/src/Avalonia.Styling/Styling/Setter.cs
+++ b/src/Avalonia.Styling/Styling/Setter.cs
@@ -4,6 +4,7 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Subjects;
+using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Metadata;
using Avalonia.Reactive;
@@ -19,6 +20,8 @@ namespace Avalonia.Styling
///
public class Setter : ISetter
{
+ private object _value;
+
///
/// Initializes a new instance of the class.
///
@@ -54,8 +57,22 @@ namespace Avalonia.Styling
[DependsOn(nameof(Property))]
public object Value
{
- get;
- set;
+ get
+ {
+ return _value;
+ }
+
+ set
+ {
+ if (value is IStyleable)
+ {
+ throw new ArgumentException(
+ "Cannot assign a control to Style.Value. Wrap the control in a .",
+ "value");
+ }
+
+ _value = value;
+ }
}
///
@@ -75,17 +92,27 @@ namespace Avalonia.Styling
throw new InvalidOperationException("Setter.Property must be set.");
}
- var binding = Value as IBinding;
+ var value = Value;
+ var binding = value as IBinding;
if (binding == null)
{
+ var template = value as ITemplate;
+
+ if (template != null)
+ {
+ var materialized = template.Build();
+ NameScope.SetNameScope((Visual)materialized, new NameScope());
+ value = materialized;
+ }
+
if (activator == null)
{
- return control.Bind(Property, ObservableEx.SingleValue(Value), BindingPriority.Style);
+ return control.Bind(Property, ObservableEx.SingleValue(value), BindingPriority.Style);
}
else
{
- var activated = new ActivatedValue(activator, Value, description);
+ var activated = new ActivatedValue(activator, value, description);
return control.Bind(Property, activated, BindingPriority.StyleTrigger);
}
}
diff --git a/src/Avalonia.Themes.Default/ListBox.xaml b/src/Avalonia.Themes.Default/ListBox.xaml
index 782cc36107..eecca02384 100644
--- a/src/Avalonia.Themes.Default/ListBox.xaml
+++ b/src/Avalonia.Themes.Default/ListBox.xaml
@@ -7,13 +7,14 @@
-
+
+ MemberSelector="{TemplateBinding MemberSelector}"
+ VirtualizationMode="{TemplateBinding VirtualizationMode}"/>
diff --git a/src/Markup/Avalonia.Markup.Xaml/Context/AvaloniaObjectAssembler.cs b/src/Markup/Avalonia.Markup.Xaml/Context/AvaloniaObjectAssembler.cs
index f5dd4b5f4b..1f5d259fc3 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Context/AvaloniaObjectAssembler.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Context/AvaloniaObjectAssembler.cs
@@ -23,6 +23,7 @@ namespace Avalonia.Markup.Xaml.Context
Settings settings = null)
{
var mapping = new DeferredLoaderMapping();
+ mapping.Map(x => x.Content, new TemplateLoader());
mapping.Map(x => x.Content, new TemplateLoader());
mapping.Map(x => x.Content, new TemplateLoader());
mapping.Map(x => x.Content, new TemplateLoader());
diff --git a/src/Markup/Avalonia.Markup.Xaml/Context/PropertyAccessor.cs b/src/Markup/Avalonia.Markup.Xaml/Context/PropertyAccessor.cs
index 20d9b07daf..e295292ba0 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Context/PropertyAccessor.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Context/PropertyAccessor.cs
@@ -136,7 +136,14 @@ namespace Avalonia.Markup.Xaml.Context
if (control != null)
{
- DelayedBinding.Add(control, property, binding);
+ if (property != Control.DataContextProperty)
+ {
+ DelayedBinding.Add(control, property, binding);
+ }
+ else
+ {
+ control.Bind(property, binding);
+ }
}
else
{
diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs
index 4b812e12f0..ec60695374 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs
@@ -20,6 +20,7 @@ namespace Avalonia.Markup.Xaml.Data
///
public Binding()
{
+ FallbackValue = AvaloniaProperty.UnsetValue;
}
///
@@ -27,6 +28,7 @@ namespace Avalonia.Markup.Xaml.Data
///
/// The binding path.
public Binding(string path)
+ : this()
{
Path = path;
}
@@ -122,12 +124,23 @@ namespace Avalonia.Markup.Xaml.Data
throw new NotSupportedException();
}
+ var fallback = FallbackValue;
+
+ // If we're binding to DataContext and our fallback is UnsetValue then override
+ // the fallback value to null, as broken bindings to DataContext must reset the
+ // DataContext in order to not propagate incorrect DataContexts to child controls.
+ // See Avalonia.Markup.Xaml.UnitTests.Data.DataContext_Binding_Should_Produce_Correct_Results.
+ if (targetProperty == Control.DataContextProperty && fallback == AvaloniaProperty.UnsetValue)
+ {
+ fallback = null;
+ }
+
var subject = new ExpressionSubject(
observer,
targetProperty?.PropertyType ?? typeof(object),
+ fallback,
Converter ?? DefaultValueConverter.Instance,
ConverterParameter,
- FallbackValue,
Priority);
return new InstancedBinding(subject, Mode, Priority);
diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
index a58a8614ee..70d3f7d161 100644
--- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
@@ -36,7 +36,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
public IValueConverter Converter { get; set; }
public object ConverterParameter { get; set; }
public string ElementName { get; set; }
- public object FallbackValue { get; set; }
+ public object FallbackValue { get; set; } = AvaloniaProperty.UnsetValue;
public BindingMode Mode { get; set; }
public string Path { get; set; }
public BindingPriority Priority { get; set; } = BindingPriority.LocalValue;
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
index 2886b63c55..1feaed1b27 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
@@ -16,6 +16,8 @@ namespace Avalonia.Markup.Xaml.Templates
[Content]
public TemplateContent Content { get; set; }
+ public bool SupportsRecycling => true;
+
public bool Match(object data)
{
if (DataType == null)
@@ -28,11 +30,6 @@ namespace Avalonia.Markup.Xaml.Templates
}
}
- public IControl Build(object data)
- {
- var visualTreeForItem = Content.Load();
- visualTreeForItem.DataContext = data;
- return visualTreeForItem;
- }
+ public IControl Build(object data) => Content.Load();
}
}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/FocusAdornerTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/FocusAdornerTemplate.cs
index a59e4231de..48309570cf 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/FocusAdornerTemplate.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Templates/FocusAdornerTemplate.cs
@@ -3,17 +3,11 @@
using Avalonia.Controls;
using Avalonia.Metadata;
+using Avalonia.Styling;
namespace Avalonia.Markup.Xaml.Templates
{
- public class FocusAdornerTemplate : ITemplate
+ public class FocusAdornerTemplate : Template
{
- [Content]
- public TemplateContent Content { get; set; }
-
- public IControl Build()
- {
- return Content.Load();
- }
}
}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs
index d5811924f2..c4d19e5a6e 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs
@@ -1,11 +1,9 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Controls;
using Avalonia.Metadata;
+using Avalonia.Styling;
namespace Avalonia.Markup.Xaml.Templates
{
@@ -18,5 +16,7 @@ namespace Avalonia.Markup.Xaml.Templates
{
return (IPanel)Content.Load();
}
+
+ object ITemplate.Build() => Build();
}
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs
index 53edea6ce4..a34093037f 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs
@@ -1,10 +1,19 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using Avalonia.Controls;
+using Avalonia.Metadata;
+using Avalonia.Styling;
+
namespace Avalonia.Markup.Xaml.Templates
{
- public class Template
+ public class Template : ITemplate
{
+ [Content]
public TemplateContent Content { get; set; }
+
+ public IControl Build() => Content.Load();
+
+ object ITemplate.Build() => Build();
}
}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
index 3c4d3c5baa..f37d6c8782 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
@@ -24,6 +24,8 @@ namespace Avalonia.Markup.Xaml.Templates
[AssignBinding]
public Binding ItemsSource { get; set; }
+ public bool SupportsRecycling => true;
+
public bool Match(object data)
{
if (DataType == null)
@@ -41,7 +43,7 @@ namespace Avalonia.Markup.Xaml.Templates
if (ItemsSource != null)
{
var obs = new ExpressionObserver(item, ItemsSource.Path);
- return new InstancedBinding(obs);
+ return new InstancedBinding(obs, BindingMode.OneWay, BindingPriority.Style);
}
return null;
diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs b/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs
index 05bf818aad..0a3be26c18 100644
--- a/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs
+++ b/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs
@@ -41,16 +41,36 @@ namespace Avalonia.Markup.Data
///
/// A parameter to pass to .
///
+ /// The binding priority.
+ public ExpressionSubject(
+ ExpressionObserver inner,
+ Type targetType,
+ IValueConverter converter,
+ object converterParameter = null,
+ BindingPriority priority = BindingPriority.LocalValue)
+ : this(inner, targetType, AvaloniaProperty.UnsetValue, converter, converterParameter, priority)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The .
+ /// The type to convert the value to.
///
/// The value to use when the binding is unable to produce a value.
///
+ /// The value converter to use.
+ ///
+ /// A parameter to pass to .
+ ///
/// The binding priority.
public ExpressionSubject(
ExpressionObserver inner,
- Type targetType,
+ Type targetType,
+ object fallbackValue,
IValueConverter converter,
object converterParameter = null,
- object fallbackValue = null,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires(inner != null);
@@ -117,7 +137,7 @@ namespace Avalonia.Markup.Data
_inner.Expression,
error.Exception.Message);
- if (_fallbackValue != null)
+ if (_fallbackValue != AvaloniaProperty.UnsetValue)
{
if (TypeUtilities.TryConvert(
type,
@@ -162,7 +182,7 @@ namespace Avalonia.Markup.Data
ConverterParameter,
CultureInfo.CurrentUICulture);
- if (_fallbackValue != null &&
+ if (_fallbackValue != AvaloniaProperty.UnsetValue &&
(converted == AvaloniaProperty.UnsetValue ||
converted is BindingError))
{
diff --git a/src/Skia/Avalonia.Skia.Android/Avalonia.Skia.Android.csproj b/src/Skia/Avalonia.Skia.Android/Avalonia.Skia.Android.csproj
index a7d30994e6..26a675dade 100644
--- a/src/Skia/Avalonia.Skia.Android/Avalonia.Skia.Android.csproj
+++ b/src/Skia/Avalonia.Skia.Android/Avalonia.Skia.Android.csproj
@@ -1,4 +1,4 @@
-
+
Debug
@@ -40,7 +40,7 @@
- ..\..\..\packages\SkiaSharp.1.49.2.1-beta\lib\MonoAndroid\SkiaSharp.dll
+ ..\..\..\packages\SkiaSharp.1.49.3.0-beta\lib\MonoAndroid\SkiaSharp.dll
True
diff --git a/src/Skia/Avalonia.Skia.Android/packages.config b/src/Skia/Avalonia.Skia.Android/packages.config
index 050c885479..374eca5c3f 100644
--- a/src/Skia/Avalonia.Skia.Android/packages.config
+++ b/src/Skia/Avalonia.Skia.Android/packages.config
@@ -1,4 +1,4 @@
-
+
\ No newline at end of file
diff --git a/src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.csproj b/src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.csproj
index 4f726bee7a..a9939c6ef6 100644
--- a/src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.csproj
+++ b/src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.csproj
@@ -1,4 +1,4 @@
-
+
@@ -57,7 +57,7 @@
- ..\..\..\packages\SkiaSharp.1.49.2.1-beta\lib\net45\SkiaSharp.dll
+ ..\..\..\packages\SkiaSharp.1.49.3.0-beta\lib\net45\SkiaSharp.dll
True
@@ -116,12 +116,12 @@
-
+
This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
-
+