Przeglądaj źródła

Sync with master.

Jumar Macato 7 lat temu
rodzic
commit
b673d44627
49 zmienionych plików z 6377 dodań i 156 usunięć
  1. 12 0
      samples/ControlCatalog/ControlCatalog.csproj
  2. 5 3
      samples/ControlCatalog/MainView.xaml
  3. 59 0
      samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
  4. 143 0
      samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs
  5. 80 0
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml
  6. 94 0
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs
  7. 3 1
      samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj
  8. 2 2
      src/Avalonia.Base/Collections/AvaloniaDictionary.cs
  9. 2726 0
      src/Avalonia.Controls/AutoCompleteBox.cs
  10. 19 29
      src/Avalonia.Controls/Border.cs
  11. 5 0
      src/Avalonia.Controls/ButtonSpinner.cs
  12. 35 0
      src/Avalonia.Controls/MenuItem.cs
  13. 998 0
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  14. 16 0
      src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs
  15. 70 83
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  16. 2 2
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  17. 25 4
      src/Avalonia.Controls/TextBox.cs
  18. 279 0
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  19. 64 0
      src/Avalonia.Controls/Utils/ISelectionAdapter.cs
  20. 342 0
      src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs
  21. 5 0
      src/Avalonia.Controls/Utils/UndoRedoHelper.cs
  22. 1 1
      src/Avalonia.Input/KeyboardDevice.cs
  23. 1 1
      src/Avalonia.Themes.Default/Accents/BaseLight.xaml
  24. 43 0
      src/Avalonia.Themes.Default/AutoCompleteBox.xaml
  25. 3 1
      src/Avalonia.Themes.Default/DefaultTheme.xaml
  26. 4 0
      src/Avalonia.Themes.Default/MenuItem.xaml
  27. 41 0
      src/Avalonia.Themes.Default/NumericUpDown.xaml
  28. 97 0
      src/Avalonia.Visuals/CornerRadius.cs
  29. 1 1
      src/Avalonia.Visuals/Media/GradientBrush.cs
  30. 1 0
      src/Avalonia.Visuals/Properties/AssemblyInfo.cs
  31. 6 1
      src/Avalonia.Visuals/Thickness.cs
  32. 1 0
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  33. 19 0
      src/Markup/Avalonia.Markup.Xaml/Converters/CornerRadiusTypeConverter.cs
  34. 1 0
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs
  35. 2 5
      src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
  36. 1 1
      tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs
  37. 1042 0
      tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
  38. 1 1
      tests/Avalonia.Controls.UnitTests/BorderTests.cs
  39. 25 0
      tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs
  40. 53 13
      tests/Avalonia.RenderTests/Controls/BorderTests.cs
  41. 1 1
      tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
  42. 1 1
      tests/Avalonia.RenderTests/Shapes/PathTests.cs
  43. 3 3
      tests/Avalonia.Styling.UnitTests/StyleTests.cs
  44. 43 0
      tests/Avalonia.Visuals.UnitTests/CornerRadiusTests.cs
  45. 2 2
      tests/Avalonia.Visuals.UnitTests/ThicknessTests.cs
  46. BIN
      tests/TestFiles/Direct2D1/Controls/Border/Border_NonUniform_CornerRadius.expected.png
  47. BIN
      tests/TestFiles/Direct2D1/Controls/Border/Border_Uniform_CornerRadius.expected.png
  48. BIN
      tests/TestFiles/Skia/Controls/Border/Border_NonUniform_CornerRadius.expected.png
  49. BIN
      tests/TestFiles/Skia/Controls/Border/Border_Uniform_CornerRadius.expected.png

+ 12 - 0
samples/ControlCatalog/ControlCatalog.csproj

@@ -41,6 +41,9 @@
     <EmbeddedResource Include="Pages\DialogsPage.xaml">
       <SubType>Designer</SubType>
     </EmbeddedResource>
+    <EmbeddedResource Include="Pages\AutoCompleteBoxPage.xaml">
+      <SubType>Designer</SubType>
+    </EmbeddedResource>
     <EmbeddedResource Include="Pages\BorderPage.xaml">
       <SubType>Designer</SubType>
     </EmbeddedResource>
@@ -78,6 +81,9 @@
     <EmbeddedResource Include="Pages\MenuPage.xaml">
       <SubType>Designer</SubType>
     </EmbeddedResource>
+    <EmbeddedResource Include="Pages\NumericUpDownPage.xaml">
+      <SubType>Designer</SubType>
+    </EmbeddedResource>
     <EmbeddedResource Include="Pages\ProgressBarPage.xaml">
       <SubType>Designer</SubType>
     </EmbeddedResource>
@@ -113,6 +119,9 @@
     <Compile Include="Pages\BorderPage.xaml.cs">
       <DependentUpon>BorderPage.xaml</DependentUpon>
     </Compile>
+    <Compile Include="Pages\AutoCompleteBoxPage.xaml.cs">
+      <DependentUpon>AutoCompleteBoxPage.xaml</DependentUpon>
+    </Compile>
     <Compile Include="Pages\ButtonPage.xaml.cs">
       <DependentUpon>ButtonPage.xaml</DependentUpon>
     </Compile>
@@ -169,6 +178,9 @@
     </Compile>
     <Compile Include="Pages\ButtonSpinnerPage.xaml.cs">
       <DependentUpon>ButtonSpinnerPage.xaml</DependentUpon>
+    </Compile>
+	<Compile Include="Pages\NumericUpDownPage.xaml.cs">
+      <DependentUpon>NumericUpDownPage.xaml</DependentUpon>
     </Compile>
     <Compile Include="Pages\ScreenPage.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />

+ 5 - 3
samples/ControlCatalog/MainView.xaml

@@ -4,11 +4,12 @@
   <TabControl Classes="sidebar" Name="Sidebar">
     <TabControl.PageTransition>
       <CrossFade Duration="0.25"/>
-    </TabControl.PageTransition>
+    </TabControl.Transition>
+    <TabItem Header="AutoCompleteBox"><pages:AutoCompleteBoxPage/></TabItem>
     <TabItem Header="Border"><pages:BorderPage/></TabItem>
     <TabItem Header="Button"><pages:ButtonPage/></TabItem>
     <TabItem Header="ButtonSpinner"><pages:ButtonSpinnerPage/></TabItem>
-    <TabItem Header="Calendar"><pages:CalendarPage/></TabItem> 
+    <TabItem Header="Calendar"><pages:CalendarPage/></TabItem>
     <TabItem Header="Canvas"><pages:CanvasPage/></TabItem>
     <TabItem Header="Carousel"><pages:CarouselPage/></TabItem>
     <TabItem Header="CheckBox"><pages:CheckBoxPage/></TabItem>
@@ -19,6 +20,7 @@
     <TabItem Header="Image"><pages:ImagePage/></TabItem>
     <TabItem Header="LayoutTransformControl"><pages:LayoutTransformControlPage/></TabItem>
     <TabItem Header="Menu"><pages:MenuPage/></TabItem>
+	  <TabItem Header="NumericUpDown"><pages:NumericUpDownPage/></TabItem>
     <TabItem Header="ProgressBar"><pages:ProgressBarPage/></TabItem>
     <TabItem Header="RadioButton"><pages:RadioButtonPage/></TabItem>
     <TabItem Header="Slider"><pages:SliderPage/></TabItem>
@@ -26,4 +28,4 @@
     <TabItem Header="ToolTip"><pages:ToolTipPage/></TabItem>
     <TabItem Header="TreeView"><pages:TreeViewPage/></TabItem>
   </TabControl>
-</UserControl>
+</UserControl>

+ 59 - 0
samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml

@@ -0,0 +1,59 @@
+<UserControl xmlns="https://github.com/avaloniaui">
+  <StackPanel Orientation="Vertical" Gap="4">
+    <TextBlock Classes="h1">AutoCompleteBox</TextBlock>
+    <TextBlock Classes="h2">A control into which the user can input text</TextBlock>
+
+    <StackPanel Orientation="Horizontal"
+              Margin="0,16,0,0"
+              HorizontalAlignment="Center"
+              Gap="8">
+      <StackPanel Orientation="Vertical">
+        <TextBlock Text="MinimumPrefixLength: 1"/>
+        <AutoCompleteBox Width="200"
+                         Margin="0,0,0,8"
+                         MinimumPrefixLength="1"/>
+        <TextBlock Text="MinimumPrefixLength: 3"/>
+        <AutoCompleteBox Width="200"
+                         Margin="0,0,0,8"
+                         MinimumPrefixLength="3"/>
+        <TextBlock Text="MinimumPopulateDelay: 1 Second"/>
+        <AutoCompleteBox Width="200"
+                         Margin="0,0,0,8"
+                         MinimumPopulateDelay="1"/>
+        <TextBlock Text="MaxDropDownHeight: 60"/>
+        <AutoCompleteBox Width="200"
+                         Margin="0,0,0,8"
+                         MaxDropDownHeight="60"/>
+        <AutoCompleteBox Width="200"
+                         Margin="0,0,0,8"
+                         Watermark="Watermark"/>
+        <TextBlock Text="Disabled"/>
+        <AutoCompleteBox Width="200"
+                         IsEnabled="False"/>
+      </StackPanel>
+      
+
+      <StackPanel Orientation="Vertical">
+        
+        <TextBlock Text="ValueMemeberSelector"/>
+        <AutoCompleteBox Width="200"
+                         Margin="0,0,0,8"
+                         ValueMemberSelector="Capital"/>
+        <TextBlock Text="ValueMemberBinding"/>
+        <AutoCompleteBox Width="200"
+                         Margin="0,0,0,8"
+                         ValueMemberBinding="{Binding Capital}"/>
+        <TextBlock Text="Multi-Binding"/>
+        <AutoCompleteBox Name="MultiBindingBox"
+                         Width="200"
+                         Margin="0,0,0,8"
+                         FilterMode="Contains"/>
+        <TextBlock Text="Async Populate"/>
+        <AutoCompleteBox Name="AsyncBox"
+                         Width="200"
+                         Margin="0,0,0,8"
+                         FilterMode="None"/>
+      </StackPanel>
+    </StackPanel>
+  </StackPanel>
+</UserControl>

+ 143 - 0
samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs

@@ -0,0 +1,143 @@
+using Avalonia.Controls;
+using Avalonia.LogicalTree;
+using Avalonia.Markup;
+using Avalonia.Markup.Xaml;
+using Avalonia.Markup.Xaml.Data;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace ControlCatalog.Pages
+{
+    public class AutoCompleteBoxPage : UserControl
+    {
+        public class StateData
+        {
+            public string Name { get; private set; }
+            public string Abbreviation { get; private set; }
+            public string Capital { get; private set; }
+
+            public StateData(string name, string abbreviatoin, string capital)
+            {
+                Name = name;
+                Abbreviation = abbreviatoin;
+                Capital = capital;
+            }
+
+            public override string ToString()
+            {
+                return Name;
+            }
+        }
+
+        private StateData[] BuildAllStates()
+        {
+            return new StateData[]
+            {
+                new StateData("Alabama","AL","Montgomery"),
+                new StateData("Alaska","AK","Juneau"),
+                new StateData("Arizona","AZ","Phoenix"),
+                new StateData("Arkansas","AR","Little Rock"),
+                new StateData("California","CA","Sacramento"),
+                new StateData("Colorado","CO","Denver"),
+                new StateData("Connecticut","CT","Hartford"),
+                new StateData("Delaware","DE","Dover"),
+                new StateData("Florida","FL","Tallahassee"),
+                new StateData("Georgia","GA","Atlanta"),
+                new StateData("Hawaii","HI","Honolulu"),
+                new StateData("Idaho","ID","Boise"),
+                new StateData("Illinois","IL","Springfield"),
+                new StateData("Indiana","IN","Indianapolis"),
+                new StateData("Iowa","IA","Des Moines"),
+                new StateData("Kansas","KS","Topeka"),
+                new StateData("Kentucky","KY","Frankfort"),
+                new StateData("Louisiana","LA","Baton Rouge"),
+                new StateData("Maine","ME","Augusta"),
+                new StateData("Maryland","MD","Annapolis"),
+                new StateData("Massachusetts","MA","Boston"),
+                new StateData("Michigan","MI","Lansing"),
+                new StateData("Minnesota","MN","St. Paul"),
+                new StateData("Mississippi","MS","Jackson"),
+                new StateData("Missouri","MO","Jefferson City"),
+                new StateData("Montana","MT","Helena"),
+                new StateData("Nebraska","NE","Lincoln"),
+                new StateData("Nevada","NV","Carson City"),
+                new StateData("New Hampshire","NH","Concord"),
+                new StateData("New Jersey","NJ","Trenton"),
+                new StateData("New Mexico","NM","Santa Fe"),
+                new StateData("New York","NY","Albany"),
+                new StateData("North Carolina","NC","Raleigh"),
+                new StateData("North Dakota","ND","Bismarck"),
+                new StateData("Ohio","OH","Columbus"),
+                new StateData("Oklahoma","OK","Oklahoma City"),
+                new StateData("Oregon","OR","Salem"),
+                new StateData("Pennsylvania","PA","Harrisburg"),
+                new StateData("Rhode Island","RI","Providence"),
+                new StateData("South Carolina","SC","Columbia"),
+                new StateData("South Dakota","SD","Pierre"),
+                new StateData("Tennessee","TN","Nashville"),
+                new StateData("Texas","TX","Austin"),
+                new StateData("Utah","UT","Salt Lake City"),
+                new StateData("Vermont","VT","Montpelier"),
+                new StateData("Virginia","VA","Richmond"),
+                new StateData("Washington","WA","Olympia"),
+                new StateData("West Virginia","WV","Charleston"),
+                new StateData("Wisconsin","WI","Madison"),
+                new StateData("Wyoming","WY","Cheyenne"),
+            };
+        }
+        public StateData[] States { get; private set; }
+        
+        public AutoCompleteBoxPage()
+        {
+            this.InitializeComponent();
+
+            States = BuildAllStates();
+
+            foreach (AutoCompleteBox box in GetAllAutoCompleteBox())
+            {
+                box.Items = States;
+            }
+
+            var converter = new FuncMultiValueConverter<string, string>(parts =>
+            {
+                return String.Format("{0} ({1})", parts.ToArray());
+            });
+            var binding = new MultiBinding { Converter = converter };
+            binding.Bindings.Add(new Binding("Name"));
+            binding.Bindings.Add(new Binding("Abbreviation"));
+
+            var multibindingBox = this.FindControl<AutoCompleteBox>("MultiBindingBox");
+            multibindingBox.ValueMemberBinding = binding;
+
+            var asyncBox = this.FindControl<AutoCompleteBox>("AsyncBox");
+            asyncBox.AsyncPopulator = PopulateAsync;
+        }
+        private IEnumerable<AutoCompleteBox> GetAllAutoCompleteBox()
+        {
+            return
+                this.GetLogicalDescendants()
+                    .OfType<AutoCompleteBox>();
+        }
+
+        private bool StringContains(string str, string query)
+        {
+            return str.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0;
+        }
+        private async Task<IEnumerable<object>> PopulateAsync(string searchText, CancellationToken cancellationToken)
+        {
+            await Task.Delay(TimeSpan.FromSeconds(1.5), cancellationToken);
+
+            return
+                States.Where(data => StringContains(data.Name, searchText) || StringContains(data.Capital, searchText))
+                      .ToList();
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+    }
+}

+ 80 - 0
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

@@ -0,0 +1,80 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+  <StackPanel Orientation="Vertical" Gap="4">
+    <TextBlock Margin="2" Classes="h1">Numeric up-down control</TextBlock>
+    <TextBlock Margin="2" Classes="h2" TextWrapping="Wrap">Numeric up-down control provides a TextBox with button spinners that allow incrementing and decrementing numeric values by using the spinner buttons, keyboard up/down arrows, or mouse wheel.</TextBlock>
+
+    <TextBlock Margin="2,5,2,2" FontSize="14" FontWeight="Bold">Features:</TextBlock>
+    <Grid Margin="2" ColumnDefinitions="Auto,Auto,Auto,Auto" RowDefinitions="Auto,Auto">
+      <Grid Grid.Row="0" Grid.Column="0" ColumnDefinitions="Auto, Auto" RowDefinitions="35,35,35,35,35">
+        <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="2">ShowButtonSpinner:</TextBlock>
+        <CheckBox Grid.Row="0" Grid.Column="1" IsChecked="{Binding #upDown.ShowButtonSpinner}" VerticalAlignment="Center" Margin="2"/>
+
+        <TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="2">IsReadOnly:</TextBlock>
+        <CheckBox Grid.Row="1" Grid.Column="1" IsChecked="{Binding #upDown.IsReadOnly}" VerticalAlignment="Center" Margin="2"/>
+
+        <TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="2">AllowSpin:</TextBlock>
+        <CheckBox Grid.Row="2" Grid.Column="1" IsChecked="{Binding #upDown.AllowSpin}" IsEnabled="{Binding #upDown.!IsReadOnly}" VerticalAlignment="Center" Margin="2"/>
+
+        <TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="2">ClipValueToMinMax:</TextBlock>
+        <CheckBox Grid.Row="3" Grid.Column="1" IsChecked="{Binding #upDown.ClipValueToMinMax}" VerticalAlignment="Center" Margin="2"/>
+
+      </Grid>
+      <Grid Grid.Row="0" Grid.Column="1" Margin="10,2,2,2" ColumnDefinitions="Auto, 120" RowDefinitions="35,35,35,35,35">
+        <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="2">FormatString:</TextBlock>
+        <DropDown Grid.Row="0" Grid.Column="1" Items="{Binding Formats}" SelectedItem="{Binding SelectedFormat}"
+                  VerticalAlignment="Center" Margin="2">
+          <DropDown.ItemTemplate>
+            <DataTemplate>
+              <StackPanel Orientation="Horizontal" Gap="2">
+                <TextBlock Text="{Binding Name}"/>
+                <TextBlock Text="-"/>
+                <TextBlock Text="{Binding Value}"/>
+              </StackPanel>
+            </DataTemplate>
+          </DropDown.ItemTemplate>
+        </DropDown>
+
+        <TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="2">ButtonSpinnerLocation:</TextBlock>
+        <DropDown Grid.Row="1" Grid.Column="1" Items="{Binding SpinnerLocations}" SelectedItem="{Binding #upDown.ButtonSpinnerLocation}"
+                  VerticalAlignment="Center" Margin="2"/>
+
+        <TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="2">CultureInfo:</TextBlock>
+        <DropDown Grid.Row="2" Grid.Column="1" Items="{Binding Cultures}" SelectedItem="{Binding #upDown.CultureInfo}"
+                  VerticalAlignment="Center" Margin="2"/>
+
+        <TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="2">Watermark:</TextBlock>
+        <TextBox Grid.Row="3" Grid.Column="1" Text="{Binding #upDown.Watermark}" VerticalAlignment="Center" Margin="2" />
+
+        <TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center" Margin="2">Text:</TextBlock>
+        <TextBox Grid.Row="4" Grid.Column="1" Text="{Binding #upDown.Text}" VerticalAlignment="Center" Margin="2" />
+      </Grid>
+      <Grid Grid.Row="0" Grid.Column="2" Margin="10,2,2,2" RowDefinitions="35,35,35,35,35" ColumnDefinitions="Auto, 120">
+        <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Minimum:</TextBlock>
+        <NumericUpDown Grid.Row="0" Grid.Column="1" Value="{Binding #upDown.Minimum}"
+                       CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/>
+
+        <TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Maximum:</TextBlock>
+        <NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding #upDown.Maximum}"
+                       CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/>
+
+        <TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Increment:</TextBlock>
+        <NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding #upDown.Increment}" VerticalAlignment="Center"
+                       Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/>
+
+        <TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Value:</TextBlock>
+        <NumericUpDown Grid.Row="3" Grid.Column="1" Value="{Binding #upDown.Value}" VerticalAlignment="Center"
+                       Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/>
+
+      </Grid>
+    </Grid>
+
+    <StackPanel Margin="2,10,2,2" Orientation="Horizontal" Gap="10">
+      <TextBlock FontSize="14" FontWeight="Bold" VerticalAlignment="Center">Usage of NumericUpDown:</TextBlock>
+      <NumericUpDown Name="upDown" Minimum="0" Maximum="10" Increment="0.5"
+                     CultureInfo="en-US" VerticalAlignment="Center" Height="25" Width="100"
+                     Watermark="Enter text" FormatString="{Binding SelectedFormat.Value}"/>
+    </StackPanel>
+
+  </StackPanel>
+</UserControl>

+ 94 - 0
samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs

@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Markup.Xaml;
+using ReactiveUI;
+
+namespace ControlCatalog.Pages
+{
+    public class NumericUpDownPage : UserControl
+    {
+        public NumericUpDownPage()
+        {
+            this.InitializeComponent();
+            var viewModel = new NumbersPageViewModel();
+            DataContext = viewModel;
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+
+    }
+
+    public class NumbersPageViewModel : ReactiveObject
+    {
+        private IList<FormatObject> _formats;
+        private FormatObject _selectedFormat;
+        private IList<Location> _spinnerLocations;
+
+        public NumbersPageViewModel()
+        {
+            SelectedFormat = Formats.FirstOrDefault();
+        }
+
+        public IList<FormatObject> Formats
+        {
+            get
+            {
+                return _formats ?? (_formats = new List<FormatObject>()
+                {
+                    new FormatObject() {Name = "Currency", Value = "C2"},
+                    new FormatObject() {Name = "Fixed point", Value = "F2"},
+                    new FormatObject() {Name = "General", Value = "G"},
+                    new FormatObject() {Name = "Number", Value = "N"},
+                    new FormatObject() {Name = "Percent", Value = "P"},
+                    new FormatObject() {Name = "Degrees", Value = "{0:N2} °"},
+                });
+            }
+        }
+
+        public IList<Location> SpinnerLocations
+        {
+            get
+            {
+                if (_spinnerLocations == null)
+                {
+                    _spinnerLocations = new List<Location>();
+                    foreach (Location value in Enum.GetValues(typeof(Location)))
+                    {
+                        _spinnerLocations.Add(value);
+                    }
+                }
+                return _spinnerLocations ;
+            }
+        }
+
+        public IList<CultureInfo> Cultures { get; } = new List<CultureInfo>()
+        {
+            new CultureInfo("en-US"),
+            new CultureInfo("en-GB"),
+            new CultureInfo("fr-FR"),
+            new CultureInfo("ar-DZ"),
+            new CultureInfo("zh-CN"),
+            new CultureInfo("cs-CZ")
+        };
+
+        public FormatObject SelectedFormat
+        {
+            get { return _selectedFormat; }
+            set { this.RaiseAndSetIfChanged(ref _selectedFormat, value); }
+        }
+    }
+
+    public class FormatObject
+    {
+        public string Value { get; set; }
+        public string Name { get; set; }
+    }
+}

+ 3 - 1
samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj

@@ -17,7 +17,9 @@
       <None Remove="MiniCube.fx" />
     </ItemGroup>
     <ItemGroup>
-      <EmbeddedResource Include="MiniCube.fx" />
+      <EmbeddedResource Include="MiniCube.fx">
+        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+      </EmbeddedResource>
     </ItemGroup>
     <ItemGroup>
         <ProjectReference Include="..\..\..\src\Avalonia.DesignerSupport\Avalonia.DesignerSupport.csproj" />

+ 2 - 2
src/Avalonia.Base/Collections/AvaloniaDictionary.cs

@@ -117,7 +117,7 @@ namespace Avalonia.Collections
             _inner = new Dictionary<TKey, TValue>();
 
             PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Count"));
-            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[]"));
+            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]"));
             
 
             if (CollectionChanged != null)
@@ -222,4 +222,4 @@ namespace Avalonia.Collections
             }
         }
     }
-}
+}

+ 2726 - 0
src/Avalonia.Controls/AutoCompleteBox.cs

@@ -0,0 +1,2726 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Avalonia.Collections;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
+using Avalonia.Controls.Utils;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Provides data for the
+    /// <see cref="E:Avalonia.Controls.AutoCompleteBox.Populated" />
+    /// event.
+    /// </summary>
+    public class PopulatedEventArgs : EventArgs
+    {
+        /// <summary>
+        /// Gets the list of possible matches added to the drop-down portion of
+        /// the <see cref="T:Avalonia.Controls.AutoCompleteBox" />
+        /// control.
+        /// </summary>
+        /// <value>The list of possible matches added to the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" />.</value>
+        public IEnumerable Data { get; private set; }
+
+        /// <summary>
+        /// Initializes a new instance of the
+        /// <see cref="T:Avalonia.Controls.PopulatedEventArgs" />.
+        /// </summary>
+        /// <param name="data">The list of possible matches added to the
+        /// drop-down portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.</param>
+        public PopulatedEventArgs(IEnumerable data)
+        {
+            Data = data;
+        }
+    }
+
+    /// <summary>
+    /// Provides data for the
+    /// <see cref="E:Avalonia.Controls.AutoCompleteBox.Populating" />
+    /// event.
+    /// </summary>
+    /// <QualityBand>Stable</QualityBand>
+    public class PopulatingEventArgs : CancelEventArgs
+    {
+        /// <summary>
+        /// Gets the text that is used to determine which items to display in
+        /// the <see cref="T:Avalonia.Controls.AutoCompleteBox" />
+        /// control.
+        /// </summary>
+        /// <value>The text that is used to determine which items to display in
+        /// the <see cref="T:Avalonia.Controls.AutoCompleteBox" />.</value>
+        public string Parameter { get; private set; }
+
+        /// <summary>
+        /// Initializes a new instance of the
+        /// <see cref="T:Avalonia.Controls.PopulatingEventArgs" />.
+        /// </summary>
+        /// <param name="parameter">The value of the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.SearchText" />
+        /// property, which is used to filter items for the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.</param>
+        public PopulatingEventArgs(string parameter)
+        {
+            Parameter = parameter;
+        }
+    }
+
+    /// <summary>
+    /// Represents the filter used by the
+    /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control to
+    /// determine whether an item is a possible match for the specified text.
+    /// </summary>
+    /// <returns>true to indicate <paramref name="item" /> is a possible match
+    /// for <paramref name="search" />; otherwise false.</returns>
+    /// <param name="search">The string used as the basis for filtering.</param>
+    /// <param name="item">The item that is compared with the
+    /// <paramref name="search" /> parameter.</param>
+    /// <typeparam name="T">The type used for filtering the
+    /// <see cref="T:Avalonia.Controls.AutoCompleteBox" />. This type can
+    /// be either a string or an object.</typeparam>
+    /// <QualityBand>Stable</QualityBand>
+    public delegate bool AutoCompleteFilterPredicate<T>(string search, T item);
+
+    /// <summary>
+    /// Specifies how text in the text box portion of the
+    /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control is used
+    /// to filter items specified by the
+    /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
+    /// property for display in the drop-down.
+    /// </summary>
+    /// <QualityBand>Stable</QualityBand>
+    public enum AutoCompleteFilterMode
+    {
+        /// <summary>
+        /// Specifies that no filter is used. All items are returned.
+        /// </summary>
+        None = 0,
+
+        /// <summary>
+        /// Specifies a culture-sensitive, case-insensitive filter where the
+        /// returned items start with the specified text. The filter uses the
+        /// <see cref="M:System.String.StartsWith(System.String,System.StringComparison)" />
+        /// method, specifying
+        /// <see cref="P:System.StringComparer.CurrentCultureIgnoreCase" /> as
+        /// the string comparison criteria.
+        /// </summary>
+        StartsWith = 1,
+
+        /// <summary>
+        /// Specifies a culture-sensitive, case-sensitive filter where the
+        /// returned items start with the specified text. The filter uses the
+        /// <see cref="M:System.String.StartsWith(System.String,System.StringComparison)" />
+        /// method, specifying
+        /// <see cref="P:System.StringComparer.CurrentCulture" /> as the string
+        /// comparison criteria.
+        /// </summary>
+        StartsWithCaseSensitive = 2,
+
+        /// <summary>
+        /// Specifies an ordinal, case-insensitive filter where the returned
+        /// items start with the specified text. The filter uses the
+        /// <see cref="M:System.String.StartsWith(System.String,System.StringComparison)" />
+        /// method, specifying
+        /// <see cref="P:System.StringComparer.OrdinalIgnoreCase" /> as the
+        /// string comparison criteria.
+        /// </summary>
+        StartsWithOrdinal = 3,
+
+        /// <summary>
+        /// Specifies an ordinal, case-sensitive filter where the returned items
+        /// start with the specified text. The filter uses the
+        /// <see cref="M:System.String.StartsWith(System.String,System.StringComparison)" />
+        /// method, specifying <see cref="P:System.StringComparer.Ordinal" /> as
+        /// the string comparison criteria.
+        /// </summary>
+        StartsWithOrdinalCaseSensitive = 4,
+
+        /// <summary>
+        /// Specifies a culture-sensitive, case-insensitive filter where the
+        /// returned items contain the specified text.
+        /// </summary>
+        Contains = 5,
+
+        /// <summary>
+        /// Specifies a culture-sensitive, case-sensitive filter where the
+        /// returned items contain the specified text.
+        /// </summary>
+        ContainsCaseSensitive = 6,
+
+        /// <summary>
+        /// Specifies an ordinal, case-insensitive filter where the returned
+        /// items contain the specified text.
+        /// </summary>
+        ContainsOrdinal = 7,
+
+        /// <summary>
+        /// Specifies an ordinal, case-sensitive filter where the returned items
+        /// contain the specified text.
+        /// </summary>
+        ContainsOrdinalCaseSensitive = 8,
+
+        /// <summary>
+        /// Specifies a culture-sensitive, case-insensitive filter where the
+        /// returned items equal the specified text. The filter uses the
+        /// <see cref="M:System.String.Equals(System.String,System.StringComparison)" />
+        /// method, specifying
+        /// <see cref="P:System.StringComparer.CurrentCultureIgnoreCase" /> as
+        /// the search comparison criteria.
+        /// </summary>
+        Equals = 9,
+
+        /// <summary>
+        /// Specifies a culture-sensitive, case-sensitive filter where the
+        /// returned items equal the specified text. The filter uses the
+        /// <see cref="M:System.String.Equals(System.String,System.StringComparison)" />
+        /// method, specifying
+        /// <see cref="P:System.StringComparer.CurrentCulture" /> as the string
+        /// comparison criteria.
+        /// </summary>
+        EqualsCaseSensitive = 10,
+
+        /// <summary>
+        /// Specifies an ordinal, case-insensitive filter where the returned
+        /// items equal the specified text. The filter uses the
+        /// <see cref="M:System.String.Equals(System.String,System.StringComparison)" />
+        /// method, specifying
+        /// <see cref="P:System.StringComparer.OrdinalIgnoreCase" /> as the
+        /// string comparison criteria.
+        /// </summary>
+        EqualsOrdinal = 11,
+
+        /// <summary>
+        /// Specifies an ordinal, case-sensitive filter where the returned items
+        /// equal the specified text. The filter uses the
+        /// <see cref="M:System.String.Equals(System.String,System.StringComparison)" />
+        /// method, specifying <see cref="P:System.StringComparer.Ordinal" /> as
+        /// the string comparison criteria.
+        /// </summary>
+        EqualsOrdinalCaseSensitive = 12,
+
+        /// <summary>
+        /// Specifies that a custom filter is used. This mode is used when the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.TextFilter" />
+        /// or
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemFilter" />
+        /// properties are set.
+        /// </summary>
+        Custom = 13,
+    }
+
+    /// <summary>
+    /// Represents a control that provides a text box for user input and a
+    /// drop-down that contains possible matches based on the input in the text
+    /// box.
+    /// </summary>
+    public class AutoCompleteBox : TemplatedControl
+    {
+        /// <summary>
+        /// Specifies the name of the selection adapter TemplatePart.
+        /// </summary>
+        private const string ElementSelectionAdapter = "PART_SelectionAdapter";
+
+        /// <summary>
+        /// Specifies the name of the Selector TemplatePart.
+        /// </summary>
+        private const string ElementSelector = "PART_SelectingItemsControl";
+
+        /// <summary>
+        /// Specifies the name of the Popup TemplatePart.
+        /// </summary>
+        private const string ElementPopup = "PART_Popup";
+
+        /// <summary>
+        /// The name for the text box part.
+        /// </summary>
+        private const string ElementTextBox = "PART_TextBox";
+
+        private IEnumerable _itemsEnumerable;
+
+        /// <summary>
+        /// Gets or sets a local cached copy of the items data.
+        /// </summary>
+        private List<object> _items;
+
+        /// <summary>
+        /// Gets or sets the observable collection that contains references to
+        /// all of the items in the generated view of data that is provided to
+        /// the selection-style control adapter.
+        /// </summary>
+        private AvaloniaList<object> _view;
+
+        /// <summary>
+        /// Gets or sets a value to ignore a number of pending change handlers.
+        /// The value is decremented after each use. This is used to reset the
+        /// value of properties without performing any of the actions in their
+        /// change handlers.
+        /// </summary>
+        /// <remarks>The int is important as a value because the TextBox
+        /// TextChanged event does not immediately fire, and this will allow for
+        /// nested property changes to be ignored.</remarks>
+        private int _ignoreTextPropertyChange;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether to ignore calling a pending
+        /// change handlers.
+        /// </summary>
+        private bool _ignorePropertyChange;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether to ignore the selection
+        /// changed event.
+        /// </summary>
+        private bool _ignoreTextSelectionChange;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether to skip the text update
+        /// processing when the selected item is updated.
+        /// </summary>
+        private bool _skipSelectedItemTextUpdate;
+
+        /// <summary>
+        /// Gets or sets the last observed text box selection start location.
+        /// </summary>
+        private int _textSelectionStart;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the user initiated the
+        /// current populate call.
+        /// </summary>
+        private bool _userCalledPopulate;
+
+        /// <summary>
+        /// A value indicating whether the popup has been opened at least once.
+        /// </summary>
+        private bool _popupHasOpened;
+
+        /// <summary>
+        /// Gets or sets the DispatcherTimer used for the MinimumPopulateDelay
+        /// condition for auto completion.
+        /// </summary>
+        private DispatcherTimer _delayTimer;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether a read-only dependency
+        /// property change handler should allow the value to be set.  This is
+        /// used to ensure that read-only properties cannot be changed via
+        /// SetValue, etc.
+        /// </summary>
+        private bool _allowWrite;
+
+        /// <summary>
+        /// The TextBox template part.
+        /// </summary>
+        private TextBox _textBox;
+        private IDisposable _textBoxSubscriptions;
+
+        /// <summary>
+        /// The SelectionAdapter.
+        /// </summary>
+        private ISelectionAdapter _adapter;
+
+        /// <summary>
+        /// A control that can provide updated string values from a binding.
+        /// </summary>
+        private BindingEvaluator<string> _valueBindingEvaluator;
+
+        /// <summary>
+        /// A weak subscription for the collection changed event.
+        /// </summary>
+        private IDisposable _collectionChangeSubscription;
+
+        private IMemberSelector _valueMemberSelector;
+        private Func<string, CancellationToken, Task<IEnumerable<object>>> _asyncPopulator;
+        private CancellationTokenSource _populationCancellationTokenSource;
+
+        private bool _itemTemplateIsFromValueMemeberBinding = true;
+        private bool _settingItemTemplateFromValueMemeberBinding;
+
+        private object _selectedItem;
+        private bool _isDropDownOpen;
+        private bool _isFocused = false;
+
+        private string _text = string.Empty;
+        private string _searchText = string.Empty;
+
+        private AutoCompleteFilterPredicate<object> _itemFilter;
+        private AutoCompleteFilterPredicate<string> _textFilter = AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith);
+
+        public static readonly RoutedEvent<SelectionChangedEventArgs> SelectionChangedEvent =
+            RoutedEvent.Register<SelectionChangedEventArgs>(nameof(SelectionChanged), RoutingStrategies.Bubble, typeof(AutoCompleteBox));
+
+        public static readonly StyledProperty<string> WatermarkProperty =
+            TextBox.WatermarkProperty.AddOwner<AutoCompleteBox>();
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.MinimumPrefixLength" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.MinimumPrefixLength" />
+        /// dependency property.</value>
+        public static readonly StyledProperty<int> MinimumPrefixLengthProperty =
+            AvaloniaProperty.Register<AutoCompleteBox, int>(
+                nameof(MinimumPrefixLength), 1,
+                validate: ValidateMinimumPrefixLength);
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.MinimumPopulateDelay" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.MinimumPopulateDelay" />
+        /// dependency property.</value>
+        public static readonly StyledProperty<TimeSpan> MinimumPopulateDelayProperty =
+            AvaloniaProperty.Register<AutoCompleteBox, TimeSpan>(
+                nameof(MinimumPopulateDelay),
+                TimeSpan.Zero,
+                validate: ValidateMinimumPopulateDelay);
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.MaxDropDownHeight" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.MaxDropDownHeight" />
+        /// dependency property.</value>
+        public static readonly StyledProperty<double> MaxDropDownHeightProperty =
+            AvaloniaProperty.Register<AutoCompleteBox, double>(
+                nameof(MaxDropDownHeight),
+                double.PositiveInfinity,
+                validate: ValidateMaxDropDownHeight);
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.IsTextCompletionEnabled" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.IsTextCompletionEnabled" />
+        /// dependency property.</value>
+        public static readonly StyledProperty<bool> IsTextCompletionEnabledProperty =
+            AvaloniaProperty.Register<AutoCompleteBox, bool>(nameof(IsTextCompletionEnabled));
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemTemplate" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemTemplate" />
+        /// dependency property.</value>
+        public static readonly StyledProperty<IDataTemplate> ItemTemplateProperty =
+            AvaloniaProperty.Register<AutoCompleteBox, IDataTemplate>(nameof(ItemTemplate));
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.IsDropDownOpen" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.IsDropDownOpen" />
+        /// dependency property.</value>
+        public static readonly DirectProperty<AutoCompleteBox, bool> IsDropDownOpenProperty =
+            AvaloniaProperty.RegisterDirect<AutoCompleteBox, bool>(
+                nameof(IsDropDownOpen),
+                o => o.IsDropDownOpen,
+                (o, v) => o.IsDropDownOpen = v);
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.SelectedItem" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.SelectedItem" />
+        /// dependency property.</value>
+        public static readonly DirectProperty<AutoCompleteBox, object> SelectedItemProperty =
+            AvaloniaProperty.RegisterDirect<AutoCompleteBox, object>(
+                nameof(SelectedItem),
+                o => o.SelectedItem,
+                (o, v) => o.SelectedItem = v);
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.Text" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.Text" />
+        /// dependency property.</value>
+        public static readonly DirectProperty<AutoCompleteBox, string> TextProperty =
+            AvaloniaProperty.RegisterDirect<AutoCompleteBox, string>(
+                nameof(Text),
+                o => o.Text,
+                (o, v) => o.Text = v);
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.SearchText" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.SearchText" />
+        /// dependency property.</value>
+        public static readonly DirectProperty<AutoCompleteBox, string> SearchTextProperty =
+            AvaloniaProperty.RegisterDirect<AutoCompleteBox, string>(
+                nameof(SearchText),
+                o => o.SearchText,
+                unsetValue: string.Empty);
+
+        /// <summary>
+        /// Gets the identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.FilterMode" />
+        /// dependency property.
+        /// </summary>
+        public static readonly StyledProperty<AutoCompleteFilterMode> FilterModeProperty =
+            AvaloniaProperty.Register<AutoCompleteBox, AutoCompleteFilterMode>(
+                nameof(FilterMode),
+                defaultValue: AutoCompleteFilterMode.StartsWith,
+                validate: ValidateFilterMode);
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemFilter" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemFilter" />
+        /// dependency property.</value>
+        public static readonly DirectProperty<AutoCompleteBox, AutoCompleteFilterPredicate<object>> ItemFilterProperty =
+            AvaloniaProperty.RegisterDirect<AutoCompleteBox, AutoCompleteFilterPredicate<object>>(
+                nameof(ItemFilter),
+                o => o.ItemFilter,
+                (o, v) => o.ItemFilter = v);
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.TextFilter" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.TextFilter" />
+        /// dependency property.</value>
+        public static readonly DirectProperty<AutoCompleteBox, AutoCompleteFilterPredicate<string>> TextFilterProperty =
+            AvaloniaProperty.RegisterDirect<AutoCompleteBox, AutoCompleteFilterPredicate<string>>(
+                nameof(TextFilter),
+                o => o.TextFilter,
+                (o, v) => o.TextFilter = v,
+                unsetValue: AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith));
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
+        /// dependency property.</value>
+        public static readonly DirectProperty<AutoCompleteBox, IEnumerable> ItemsProperty =
+            AvaloniaProperty.RegisterDirect<AutoCompleteBox, IEnumerable>(
+                nameof(Items),
+                o => o.Items,
+                (o, v) => o.Items = v);
+
+        public static readonly DirectProperty<AutoCompleteBox, IMemberSelector> ValueMemberSelectorProperty =
+            AvaloniaProperty.RegisterDirect<AutoCompleteBox, IMemberSelector>(
+                nameof(ValueMemberSelector),
+                o => o.ValueMemberSelector,
+                (o, v) => o.ValueMemberSelector = v);
+
+        public static readonly DirectProperty<AutoCompleteBox, Func<string, CancellationToken, Task<IEnumerable<object>>>> AsyncPopulatorProperty =
+            AvaloniaProperty.RegisterDirect<AutoCompleteBox, Func<string, CancellationToken, Task<IEnumerable<object>>>>(
+                nameof(AsyncPopulator),
+                o => o.AsyncPopulator,
+                (o, v) => o.AsyncPopulator = v);
+
+        private static int ValidateMinimumPrefixLength(AutoCompleteBox control, int value)
+        {
+            Contract.Requires<ArgumentOutOfRangeException>(value >= -1);
+
+            return value;
+        }
+
+        private static TimeSpan ValidateMinimumPopulateDelay(AutoCompleteBox control, TimeSpan value)
+        {
+            Contract.Requires<ArgumentOutOfRangeException>(value.TotalMilliseconds >= 0.0);
+
+            return value;
+        }
+
+        private static double ValidateMaxDropDownHeight(AutoCompleteBox control, double value)
+        {
+            Contract.Requires<ArgumentOutOfRangeException>(value >= 0.0);
+
+            return value;
+        }
+
+        private static bool IsValidFilterMode(AutoCompleteFilterMode mode)
+        {
+            switch (mode)
+            {
+                case AutoCompleteFilterMode.None:
+                case AutoCompleteFilterMode.StartsWith:
+                case AutoCompleteFilterMode.StartsWithCaseSensitive:
+                case AutoCompleteFilterMode.StartsWithOrdinal:
+                case AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive:
+                case AutoCompleteFilterMode.Contains:
+                case AutoCompleteFilterMode.ContainsCaseSensitive:
+                case AutoCompleteFilterMode.ContainsOrdinal:
+                case AutoCompleteFilterMode.ContainsOrdinalCaseSensitive:
+                case AutoCompleteFilterMode.Equals:
+                case AutoCompleteFilterMode.EqualsCaseSensitive:
+                case AutoCompleteFilterMode.EqualsOrdinal:
+                case AutoCompleteFilterMode.EqualsOrdinalCaseSensitive:
+                case AutoCompleteFilterMode.Custom:
+                    return true;
+                default:
+                    return false;
+            }
+        }
+        private static AutoCompleteFilterMode ValidateFilterMode(AutoCompleteBox control, AutoCompleteFilterMode value)
+        {
+            Contract.Requires<ArgumentException>(IsValidFilterMode(value));
+
+            return value;
+        }
+
+        /// <summary>
+        /// Handle the change of the IsEnabled property.
+        /// </summary>
+        /// <param name="e">The event data.</param>
+        private void OnControlIsEnabledChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            bool isEnabled = (bool)e.NewValue;
+            if (!isEnabled)
+            {
+                IsDropDownOpen = false;
+            }
+        }
+
+        /// <summary>
+        /// MinimumPopulateDelayProperty property changed handler. Any current
+        /// dispatcher timer will be stopped. The timer will not be restarted
+        /// until the next TextUpdate call by the user.
+        /// </summary>
+        /// <param name="e">Event arguments.</param>
+        private void OnMinimumPopulateDelayChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            var newValue = (TimeSpan)e.NewValue;
+
+            // Stop any existing timer
+            if (_delayTimer != null)
+            {
+                _delayTimer.Stop();
+
+                if (newValue == TimeSpan.Zero)
+                {
+                    _delayTimer = null;
+                }
+            }
+
+            if (newValue > TimeSpan.Zero)
+            {
+                // Create or clear a dispatcher timer instance
+                if (_delayTimer == null)
+                {
+                    _delayTimer = new DispatcherTimer();
+                    _delayTimer.Tick += PopulateDropDown;
+                }
+
+                // Set the new tick interval
+                _delayTimer.Interval = newValue;
+            }
+        }
+
+        /// <summary>
+        /// IsDropDownOpenProperty property changed handler.
+        /// </summary>
+        /// <param name="e">Event arguments.</param>
+        private void OnIsDropDownOpenChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            // Ignore the change if requested
+            if (_ignorePropertyChange)
+            {
+                _ignorePropertyChange = false;
+                return;
+            }
+
+            bool oldValue = (bool)e.OldValue;
+            bool newValue = (bool)e.NewValue;
+
+            if (newValue)
+            {
+                TextUpdated(Text, true);
+            }
+            else
+            {
+                ClosingDropDown(oldValue);
+            }
+
+            UpdatePseudoClasses();
+        }
+
+        private void OnSelectedItemPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (_ignorePropertyChange)
+            {
+                _ignorePropertyChange = false;
+                return;
+            }
+
+            // Update the text display
+            if (_skipSelectedItemTextUpdate)
+            {
+                _skipSelectedItemTextUpdate = false;
+            }
+            else
+            {
+                OnSelectedItemChanged(e.NewValue);
+            }
+
+            // Fire the SelectionChanged event
+            List<object> removed = new List<object>();
+            if (e.OldValue != null)
+            {
+                removed.Add(e.OldValue);
+            }
+
+            List<object> added = new List<object>();
+            if (e.NewValue != null)
+            {
+                added.Add(e.NewValue);
+            }
+
+            OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, removed, added));
+        }
+
+        /// <summary>
+        /// TextProperty property changed handler.
+        /// </summary>
+        /// <param name="e">Event arguments.</param>
+        private void OnTextPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            TextUpdated((string)e.NewValue, false);
+        }
+
+        private void OnSearchTextPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (_ignorePropertyChange)
+            {
+                _ignorePropertyChange = false;
+                return;
+            }
+
+            // Ensure the property is only written when expected
+            if (!_allowWrite)
+            {
+                // Reset the old value before it was incorrectly written
+                _ignorePropertyChange = true;
+                SetValue(e.Property, e.OldValue);
+
+                throw new InvalidOperationException("Cannot set read-only property SearchText.");
+            }
+        }
+
+        /// <summary>
+        /// FilterModeProperty property changed handler.
+        /// </summary>
+        /// <param name="e">Event arguments.</param>
+        private void OnFilterModePropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            AutoCompleteFilterMode mode = (AutoCompleteFilterMode)e.NewValue;
+
+            // Sets the filter predicate for the new value
+            TextFilter = AutoCompleteSearch.GetFilter(mode);
+        }
+
+        /// <summary>
+        /// ItemFilterProperty property changed handler.
+        /// </summary>
+        /// <param name="e">Event arguments.</param>
+        private void OnItemFilterPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            AutoCompleteFilterPredicate<object> value = e.NewValue as AutoCompleteFilterPredicate<object>;
+
+            // If null, revert to the "None" predicate
+            if (value == null)
+            {
+                FilterMode = AutoCompleteFilterMode.None;
+            }
+            else
+            {
+                FilterMode = AutoCompleteFilterMode.Custom;
+                TextFilter = null;
+            }
+        }
+
+        /// <summary>
+        /// ItemsSourceProperty property changed handler.
+        /// </summary>
+        /// <param name="e">Event arguments.</param>
+        private void OnItemsPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            OnItemsChanged((IEnumerable)e.NewValue);
+        }
+
+        private void OnItemTemplatePropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (!_settingItemTemplateFromValueMemeberBinding)
+                _itemTemplateIsFromValueMemeberBinding = false;
+        }
+        private void OnValueMemberBindingChanged(IBinding value)
+        {
+            if(_itemTemplateIsFromValueMemeberBinding)
+            {
+                var template =
+                    new FuncDataTemplate(
+                        typeof(object),
+                        o =>
+                        {
+                            var control = new ContentControl();
+                            control.Bind(ContentControl.ContentProperty, value);
+                            return control;
+                        });
+
+                _settingItemTemplateFromValueMemeberBinding = true;
+                ItemTemplate = template;
+                _settingItemTemplateFromValueMemeberBinding = false;
+            }
+        }
+
+        static AutoCompleteBox()
+        {
+            FocusableProperty.OverrideDefaultValue<AutoCompleteBox>(true);
+
+            MinimumPopulateDelayProperty.Changed.AddClassHandler<AutoCompleteBox>(x => x.OnMinimumPopulateDelayChanged);
+            IsDropDownOpenProperty.Changed.AddClassHandler<AutoCompleteBox>(x => x.OnIsDropDownOpenChanged);
+            SelectedItemProperty.Changed.AddClassHandler<AutoCompleteBox>(x => x.OnSelectedItemPropertyChanged);
+            TextProperty.Changed.AddClassHandler<AutoCompleteBox>(x => x.OnTextPropertyChanged);
+            SearchTextProperty.Changed.AddClassHandler<AutoCompleteBox>(x => x.OnSearchTextPropertyChanged);
+            FilterModeProperty.Changed.AddClassHandler<AutoCompleteBox>(x => x.OnFilterModePropertyChanged);
+            ItemFilterProperty.Changed.AddClassHandler<AutoCompleteBox>(x => x.OnItemFilterPropertyChanged);
+            ItemsProperty.Changed.AddClassHandler<AutoCompleteBox>(x => x.OnItemsPropertyChanged);
+            IsEnabledProperty.Changed.AddClassHandler<AutoCompleteBox>(x => x.OnControlIsEnabledChanged);
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> class.
+        /// </summary>
+        public AutoCompleteBox()
+        {
+            ClearView();
+        }
+
+        /// <summary>
+        /// Gets or sets the minimum number of characters required to be entered
+        /// in the text box before the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> displays
+        /// possible matches.
+        /// matches.
+        /// </summary>
+        /// <value>
+        /// The minimum number of characters to be entered in the text box
+        /// before the <see cref="T:Avalonia.Controls.AutoCompleteBox" />
+        /// displays possible matches. The default is 1.
+        /// </value>
+        /// <remarks>
+        /// If you set MinimumPrefixLength to -1, the AutoCompleteBox will
+        /// not provide possible matches. There is no maximum value, but
+        /// setting MinimumPrefixLength to value that is too large will
+        /// prevent the AutoCompleteBox from providing possible matches as well.
+        /// </remarks>
+        public int MinimumPrefixLength
+        {
+            get { return GetValue(MinimumPrefixLengthProperty); }
+            set { SetValue(MinimumPrefixLengthProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the first possible match
+        /// found during the filtering process will be displayed automatically
+        /// in the text box.
+        /// </summary>
+        /// <value>
+        /// True if the first possible match found will be displayed
+        /// automatically in the text box; otherwise, false. The default is
+        /// false.
+        /// </value>
+        public bool IsTextCompletionEnabled
+        {
+            get { return GetValue(IsTextCompletionEnabledProperty); }
+            set { SetValue(IsTextCompletionEnabledProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the <see cref="T:Avalonia.DataTemplate" /> used
+        /// to display each item in the drop-down portion of the control.
+        /// </summary>
+        /// <value>The <see cref="T:Avalonia.DataTemplate" /> used to
+        /// display each item in the drop-down. The default is null.</value>
+        /// <remarks>
+        /// You use the ItemTemplate property to specify the visualization
+        /// of the data objects in the drop-down portion of the AutoCompleteBox
+        /// control. If your AutoCompleteBox is bound to a collection and you
+        /// do not provide specific display instructions by using a
+        /// DataTemplate, the resulting UI of each item is a string
+        /// representation of each object in the underlying collection.
+        /// </remarks>
+        public IDataTemplate ItemTemplate
+        {
+            get { return GetValue(ItemTemplateProperty); }
+            set { SetValue(ItemTemplateProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the minimum delay, after text is typed
+        /// in the text box before the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control
+        /// populates the list of possible matches in the drop-down.
+        /// </summary>
+        /// <value>The minimum delay, after text is typed in
+        /// the text box, but before the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> populates
+        /// the list of possible matches in the drop-down. The default is 0.</value>
+        public TimeSpan MinimumPopulateDelay
+        {
+            get { return GetValue(MinimumPopulateDelayProperty); }
+            set { SetValue(MinimumPopulateDelayProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the maximum height of the drop-down portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.
+        /// </summary>
+        /// <value>The maximum height of the drop-down portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.
+        /// The default is <see cref="F:System.Double.PositiveInfinity" />.</value>
+        /// <exception cref="T:System.ArgumentException">The specified value is less than 0.</exception>
+        public double MaxDropDownHeight
+        {
+            get { return GetValue(MaxDropDownHeightProperty); }
+            set { SetValue(MaxDropDownHeightProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the drop-down portion of
+        /// the control is open.
+        /// </summary>
+        /// <value>
+        /// True if the drop-down is open; otherwise, false. The default is
+        /// false.
+        /// </value>
+        public bool IsDropDownOpen
+        {
+            get { return  _isDropDownOpen; }
+            set { SetAndRaise(IsDropDownOpenProperty, ref  _isDropDownOpen, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the  <see cref="T:Avalonia.Data.Binding" /> that
+        /// is used to get the values for display in the text portion of
+        /// the <see cref="T:Avalonia.Controls.AutoCompleteBox" />
+        /// control.
+        /// </summary>
+        /// <value>The <see cref="T:Avalonia.Data.IBinding" /> object used
+        /// when binding to a collection property.</value>
+        [AssignBinding]
+        public IBinding ValueMemberBinding
+        {
+            get { return _valueBindingEvaluator?.ValueBinding; }
+            set
+            {
+                if (ValueMemberBinding != value)
+                {
+                    _valueBindingEvaluator = new BindingEvaluator<string>(value);
+                    OnValueMemberBindingChanged(value);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the MemberSelector that is used to get values for
+        /// display in the text portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.
+        /// </summary>
+        /// <value>The MemberSelector that is used to get values for display in
+        /// the text portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.</value>
+        public IMemberSelector ValueMemberSelector
+        {
+            get { return _valueMemberSelector; }
+            set { SetAndRaise(ValueMemberSelectorProperty, ref _valueMemberSelector, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the selected item in the drop-down.
+        /// </summary>
+        /// <value>The selected item in the drop-down.</value>
+        /// <remarks>
+        /// If the IsTextCompletionEnabled property is true and text typed by
+        /// the user matches an item in the ItemsSource collection, which is
+        /// then displayed in the text box, the SelectedItem property will be
+        /// a null reference.
+        /// </remarks>
+        public object SelectedItem
+        {
+            get { return _selectedItem; }
+            set { SetAndRaise(SelectedItemProperty, ref _selectedItem, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the text in the text box portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.
+        /// </summary>
+        /// <value>The text in the text box portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.</value>
+        public string Text
+        {
+            get { return _text; }
+            set { SetAndRaise(TextProperty, ref _text, value); }
+        }
+
+        /// <summary>
+        /// Gets the text that is used to filter items in the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
+        /// item collection.
+        /// </summary>
+        /// <value>The text that is used to filter items in the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
+        /// item collection.</value>
+        /// <remarks>
+        /// The SearchText value is typically the same as the
+        /// Text property, but is set after the TextChanged event occurs
+        /// and before the Populating event.
+        /// </remarks>
+        public string SearchText
+        {
+            get { return _searchText; }
+            private set
+            {
+                try
+                {
+                    _allowWrite = true;
+                    SetAndRaise(SearchTextProperty, ref _searchText, value);
+                }
+                finally
+                {
+                    _allowWrite = false;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets how the text in the text box is used to filter items
+        /// specified by the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
+        /// property for display in the drop-down.
+        /// </summary>
+        /// <value>One of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteFilterMode" />
+        /// values The default is
+        /// <see cref="F:Avalonia.Controls.AutoCompleteFilterMode.StartsWith" />.</value>
+        /// <exception cref="T:System.ArgumentException">The specified value is
+        /// not a valid
+        /// <see cref="T:Avalonia.Controls.AutoCompleteFilterMode" />.</exception>
+        /// <remarks>
+        /// Use the FilterMode property to specify how possible matches are
+        /// filtered. For example, possible matches can be filtered in a
+        /// predefined or custom way. The search mode is automatically set to
+        /// Custom if you set the ItemFilter property.
+        /// </remarks>
+        public AutoCompleteFilterMode FilterMode
+        {
+            get { return GetValue(FilterModeProperty); }
+            set { SetValue(FilterModeProperty, value); }
+        }
+
+        public string Watermark
+        {
+            get { return GetValue(WatermarkProperty); }
+            set { SetValue(WatermarkProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the custom method that uses user-entered text to filter
+        /// the items specified by the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
+        /// property for display in the drop-down.
+        /// </summary>
+        /// <value>The custom method that uses the user-entered text to filter
+        /// the items specified by the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
+        /// property. The default is null.</value>
+        /// <remarks>
+        /// The filter mode is automatically set to Custom if you set the
+        /// ItemFilter property.
+        /// </remarks>
+        public AutoCompleteFilterPredicate<object> ItemFilter
+        {
+            get { return _itemFilter; }
+            set { SetAndRaise(ItemFilterProperty, ref _itemFilter, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the custom method that uses the user-entered text to
+        /// filter items specified by the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
+        /// property in a text-based way for display in the drop-down.
+        /// </summary>
+        /// <value>The custom method that uses the user-entered text to filter
+        /// items specified by the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
+        /// property in a text-based way for display in the drop-down.</value>
+        /// <remarks>
+        /// The search mode is automatically set to Custom if you set the
+        /// TextFilter property.
+        /// </remarks>
+        public AutoCompleteFilterPredicate<string> TextFilter
+        {
+            get { return _textFilter; }
+            set { SetAndRaise(TextFilterProperty, ref _textFilter, value); }
+        }
+
+        public Func<string, CancellationToken, Task<IEnumerable<object>>> AsyncPopulator
+        {
+            get { return _asyncPopulator; }
+            set { SetAndRaise(AsyncPopulatorProperty, ref _asyncPopulator, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets a collection that is used to generate the items for the
+        /// drop-down portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.
+        /// </summary>
+        /// <value>The collection that is used to generate the items of the
+        /// drop-down portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.</value>
+        public IEnumerable Items
+        {
+            get { return _itemsEnumerable; }
+            set { SetAndRaise(ItemsProperty, ref _itemsEnumerable, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the drop down popup control.
+        /// </summary>
+        private Popup DropDownPopup { get; set; }
+
+        /// <summary>
+        /// Gets or sets the Text template part.
+        /// </summary>
+        private TextBox TextBox
+        {
+            get { return _textBox; }
+            set
+            {
+                _textBoxSubscriptions?.Dispose();
+                _textBox = value;
+
+                // Attach handlers
+                if (_textBox != null)
+                {
+                    _textBoxSubscriptions =
+                        _textBox.GetObservable(TextBox.TextProperty)
+                                .Subscribe(_ => OnTextBoxTextChanged());
+
+                    if (Text != null)
+                    {
+                        UpdateTextValue(Text);
+                    }
+                }
+            }
+        }
+
+        private int TextBoxSelectionStart
+        {
+            get
+            {
+                if (TextBox != null)
+                {
+                    return Math.Min(TextBox.SelectionStart, TextBox.SelectionEnd);
+                }
+                else
+                {
+                    return 0;
+                }
+            }
+        }
+        private int TextBoxSelectionLength
+        {
+            get
+            {
+                if (TextBox != null)
+                {
+                    return Math.Abs(TextBox.SelectionEnd - TextBox.SelectionStart);
+                }
+                else
+                {
+                    return 0;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the selection adapter used to populate the drop-down
+        /// with a list of selectable items.
+        /// </summary>
+        /// <value>The selection adapter used to populate the drop-down with a
+        /// list of selectable items.</value>
+        /// <remarks>
+        /// You can use this property when you create an automation peer to
+        /// use with AutoCompleteBox or deriving from AutoCompleteBox to
+        /// create a custom control.
+        /// </remarks>
+        protected ISelectionAdapter SelectionAdapter
+        {
+            get { return _adapter; }
+            set
+            {
+                if (_adapter != null)
+                {
+                    _adapter.SelectionChanged -= OnAdapterSelectionChanged;
+                    _adapter.Commit -= OnAdapterSelectionComplete;
+                    _adapter.Cancel -= OnAdapterSelectionCanceled;
+                    _adapter.Cancel -= OnAdapterSelectionComplete;
+                    _adapter.Items = null;
+                }
+
+                _adapter = value;
+
+                if (_adapter != null)
+                {
+                    _adapter.SelectionChanged += OnAdapterSelectionChanged;
+                    _adapter.Commit += OnAdapterSelectionComplete;
+                    _adapter.Cancel += OnAdapterSelectionCanceled;
+                    _adapter.Cancel += OnAdapterSelectionComplete;
+                    _adapter.Items = _view;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Returns the
+        /// <see cref="T:Avalonia.Controls.ISelectionAdapter" /> part, if
+        /// possible.
+        /// </summary>
+        /// <returns>
+        /// A <see cref="T:Avalonia.Controls.ISelectionAdapter" /> object,
+        /// if possible. Otherwise, null.
+        /// </returns>
+        protected virtual ISelectionAdapter GetSelectionAdapterPart(INameScope nameScope)
+        {
+            ISelectionAdapter adapter = null;
+            SelectingItemsControl selector = nameScope.Find<SelectingItemsControl>(ElementSelector);
+            if (selector != null)
+            {
+                // Check if it is already an IItemsSelector
+                adapter = selector as ISelectionAdapter;
+                if (adapter == null)
+                {
+                    // Built in support for wrapping a Selector control
+                    adapter = new SelectingItemsControlSelectionAdapter(selector);
+                }
+            }
+            if (adapter == null)
+            {
+                adapter = nameScope.Find<ISelectionAdapter>(ElementSelectionAdapter);
+            }
+            return adapter;
+        }
+
+        /// <summary>
+        /// Builds the visual tree for the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control
+        /// when a new template is applied.
+        /// </summary>
+        protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
+        {
+
+            if (DropDownPopup != null)
+            {
+                DropDownPopup.Closed -= DropDownPopup_Closed;
+                DropDownPopup = null;
+            }
+
+            // Set the template parts. Individual part setters remove and add
+            // any event handlers.
+            Popup popup = e.NameScope.Find<Popup>(ElementPopup);
+            if (popup != null)
+            {
+                DropDownPopup = popup;
+                DropDownPopup.Closed += DropDownPopup_Closed;
+            }
+
+            SelectionAdapter = GetSelectionAdapterPart(e.NameScope);
+            TextBox = e.NameScope.Find<TextBox>(ElementTextBox);
+
+            // If the drop down property indicates that the popup is open,
+            // flip its value to invoke the changed handler.
+            if (IsDropDownOpen && DropDownPopup != null && !DropDownPopup.IsOpen)
+            {
+                OpeningDropDown(false);
+            }
+
+            base.OnTemplateApplied(e);
+        }
+
+        /// <summary>
+        /// Provides handling for the
+        /// <see cref="E:Avalonia.InputElement.KeyDown" /> event.
+        /// </summary>
+        /// <param name="e">A <see cref="T:Avalonia.Input.KeyEventArgs" />
+        /// that contains the event data.</param>
+        protected override void OnKeyDown(KeyEventArgs e)
+        {
+            Contract.Requires<ArgumentNullException>(e != null);
+
+            base.OnKeyDown(e);
+
+            if (e.Handled || !IsEnabled)
+            {
+                return;
+            }
+
+            // The drop down is open, pass along the key event arguments to the
+            // selection adapter. If it isn't handled by the adapter's logic,
+            // then we handle some simple navigation scenarios for controlling
+            // the drop down.
+            if (IsDropDownOpen)
+            {
+                if (SelectionAdapter != null)
+                {
+                    SelectionAdapter.HandleKeyDown(e);
+                    if (e.Handled)
+                    {
+                        return;
+                    }
+                }
+
+                if (e.Key == Key.Escape)
+                {
+                    OnAdapterSelectionCanceled(this, new RoutedEventArgs());
+                    e.Handled = true;
+                }
+            }
+            else
+            {
+                // The drop down is not open, the Down key will toggle it open.
+                if (e.Key == Key.Down)
+                {
+                    IsDropDownOpen = true;
+                    e.Handled = true;
+                }
+            }
+
+            // Standard drop down navigation
+            switch (e.Key)
+            {
+                case Key.F4:
+                    IsDropDownOpen = !IsDropDownOpen;
+                    e.Handled = true;
+                    break;
+
+                case Key.Enter:
+                    OnAdapterSelectionComplete(this, new RoutedEventArgs());
+                    e.Handled = true;
+                    break;
+
+                default:
+                    break;
+            }
+        }
+
+        /// <summary>
+        /// Provides handling for the
+        /// <see cref="E:Avalonia.UIElement.GotFocus" /> event.
+        /// </summary>
+        /// <param name="e">A <see cref="T:Avalonia.RoutedEventArgs" />
+        /// that contains the event data.</param>
+        protected override void OnGotFocus(GotFocusEventArgs e)
+        {
+            base.OnGotFocus(e);
+            FocusChanged(HasFocus());
+        }
+
+        /// <summary>
+        /// Provides handling for the
+        /// <see cref="E:Avalonia.UIElement.LostFocus" /> event.
+        /// </summary>
+        /// <param name="e">A <see cref="T:Avalonia.RoutedEventArgs" />
+        /// that contains the event data.</param>
+        protected override void OnLostFocus(RoutedEventArgs e)
+        {
+            base.OnLostFocus(e);
+            FocusChanged(HasFocus());
+        }
+
+        /// <summary>
+        /// Determines whether the text box or drop-down portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control has
+        /// focus.
+        /// </summary>
+        /// <returns>true to indicate the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> has focus;
+        /// otherwise, false.</returns>
+        protected bool HasFocus()
+        {
+            IVisual focused = FocusManager.Instance.Current;
+
+            while (focused != null)
+            {
+                if (object.ReferenceEquals(focused, this))
+                {
+                    return true;
+                }
+
+                // This helps deal with popups that may not be in the same
+                // visual tree
+                IVisual parent = focused.GetVisualParent();
+                if (parent == null)
+                {
+                    // Try the logical parent.
+                    IControl element = focused as IControl;
+                    if (element != null)
+                    {
+                        parent = element.Parent;
+                    }
+                }
+                focused = parent;
+            }
+            return false;
+        }
+
+        /// <summary>
+        /// Handles the FocusChanged event.
+        /// </summary>
+        /// <param name="hasFocus">A value indicating whether the control
+        /// currently has the focus.</param>
+        private void FocusChanged(bool hasFocus)
+        {
+            // The OnGotFocus & OnLostFocus are asynchronously and cannot
+            // reliably tell you that have the focus.  All they do is let you
+            // know that the focus changed sometime in the past.  To determine
+            // if you currently have the focus you need to do consult the
+            // FocusManager (see HasFocus()).
+
+            bool wasFocused = _isFocused;
+            _isFocused = hasFocus;
+
+            if (hasFocus)
+            {
+
+                if (!wasFocused && TextBox != null && TextBoxSelectionLength <= 0)
+                {
+                    TextBox.Focus();
+                    TextBox.SelectionStart = 0;
+                    TextBox.SelectionEnd = TextBox.Text?.Length ?? 0;
+                }
+            }
+            else
+            {
+                IsDropDownOpen = false;
+                _userCalledPopulate = false;
+                ClearTextBoxSelection();
+            }
+
+            _isFocused = hasFocus;
+        }
+
+        /// <summary>
+        /// Occurs when the text in the text box portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> changes.
+        /// </summary>
+        public event EventHandler TextChanged;
+
+        /// <summary>
+        /// Occurs when the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> is
+        /// populating the drop-down with possible matches based on the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.Text" />
+        /// property.
+        /// </summary>
+        /// <remarks>
+        /// If the event is canceled, by setting the PopulatingEventArgs.Cancel
+        /// property to true, the AutoCompleteBox will not automatically
+        /// populate the selection adapter contained in the drop-down.
+        /// In this case, if you want possible matches to appear, you must
+        /// provide the logic for populating the selection adapter.
+        /// </remarks>
+        public event EventHandler<PopulatingEventArgs> Populating;
+
+        /// <summary>
+        /// Occurs when the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> has
+        /// populated the drop-down with possible matches based on the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.Text" />
+        /// property.
+        /// </summary>
+        public event EventHandler<PopulatedEventArgs> Populated;
+
+        /// <summary>
+        /// Occurs when the value of the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.IsDropDownOpen" />
+        /// property is changing from false to true.
+        /// </summary>
+        public event EventHandler<CancelEventArgs> DropDownOpening;
+
+        /// <summary>
+        /// Occurs when the value of the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.IsDropDownOpen" />
+        /// property has changed from false to true and the drop-down is open.
+        /// </summary>
+        public event EventHandler DropDownOpened;
+
+        /// <summary>
+        /// Occurs when the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.IsDropDownOpen" />
+        /// property is changing from true to false.
+        /// </summary>
+        public event EventHandler<CancelEventArgs> DropDownClosing;
+
+        /// <summary>
+        /// Occurs when the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.IsDropDownOpen" />
+        /// property was changed from true to false and the drop-down is open.
+        /// </summary>
+        public event EventHandler DropDownClosed;
+
+        /// <summary>
+        /// Occurs when the selected item in the drop-down portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> has
+        /// changed.
+        /// </summary>
+        public event EventHandler<SelectionChangedEventArgs> SelectionChanged
+        {
+            add { AddHandler(SelectionChangedEvent, value); }
+            remove { RemoveHandler(SelectionChangedEvent, value); }
+        }
+
+        /// <summary>
+        /// Raises the
+        /// <see cref="E:Avalonia.Controls.AutoCompleteBox.Populating" />
+        /// event.
+        /// </summary>
+        /// <param name="e">A
+        /// <see cref="T:Avalonia.Controls.PopulatingEventArgs" /> that
+        /// contains the event data.</param>
+        protected virtual void OnPopulating(PopulatingEventArgs e)
+        {
+            Populating?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Raises the
+        /// <see cref="E:Avalonia.Controls.AutoCompleteBox.Populated" />
+        /// event.
+        /// </summary>
+        /// <param name="e">A
+        /// <see cref="T:Avalonia.Controls.PopulatedEventArgs" />
+        /// that contains the event data.</param>
+        protected virtual void OnPopulated(PopulatedEventArgs e)
+        {
+            Populated?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Raises the
+        /// <see cref="E:Avalonia.Controls.AutoCompleteBox.SelectionChanged" />
+        /// event.
+        /// </summary>
+        /// <param name="e">A
+        /// <see cref="T:Avalonia.Controls.SelectionChangedEventArgs" />
+        /// that contains the event data.</param>
+        protected virtual void OnSelectionChanged(SelectionChangedEventArgs e)
+        {
+            RaiseEvent(e);
+        }
+
+        /// <summary>
+        /// Raises the
+        /// <see cref="E:Avalonia.Controls.AutoCompleteBox.DropDownOpening" />
+        /// event.
+        /// </summary>
+        /// <param name="e">A
+        /// <see cref="T:Avalonia.Controls.CancelEventArgs" />
+        /// that contains the event data.</param>
+        protected virtual void OnDropDownOpening(CancelEventArgs e)
+        {
+            DropDownOpening?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Raises the
+        /// <see cref="E:Avalonia.Controls.AutoCompleteBox.DropDownOpened" />
+        /// event.
+        /// </summary>
+        /// <param name="e">A
+        /// <see cref="T:System.EventArgs" />
+        /// that contains the event data.</param>
+        protected virtual void OnDropDownOpened(EventArgs e)
+        {
+            DropDownOpened?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Raises the
+        /// <see cref="E:Avalonia.Controls.AutoCompleteBox.DropDownClosing" />
+        /// event.
+        /// </summary>
+        /// <param name="e">A
+        /// <see cref="T:Avalonia.Controls.CancelEventArgs" />
+        /// that contains the event data.</param>
+        protected virtual void OnDropDownClosing(CancelEventArgs e)
+        {
+            DropDownClosing?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Raises the
+        /// <see cref="E:Avalonia.Controls.AutoCompleteBox.DropDownClosed" />
+        /// event.
+        /// </summary>
+        /// <param name="e">A
+        /// <see cref="T:System.EventArgs" />
+        /// which contains the event data.</param>
+        protected virtual void OnDropDownClosed(EventArgs e)
+        {
+            DropDownClosed?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Raises the
+        /// <see cref="E:Avalonia.Controls.AutoCompleteBox.TextChanged" />
+        /// event.
+        /// </summary>
+        /// <param name="e">A <see cref="T:Avalonia.RoutedEventArgs" />
+        /// that contains the event data.</param>
+        protected virtual void OnTextChanged(RoutedEventArgs e)
+        {
+            TextChanged?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Begin closing the drop-down.
+        /// </summary>
+        /// <param name="oldValue">The original value.</param>
+        private void ClosingDropDown(bool oldValue)
+        {
+            var args = new CancelEventArgs();
+            OnDropDownClosing(args);
+
+            if (args.Cancel)
+            {
+                _ignorePropertyChange = true;
+                SetValue(IsDropDownOpenProperty, oldValue);
+            }
+            else
+            {
+                CloseDropDown();
+            }
+
+            UpdatePseudoClasses();
+        }
+
+        /// <summary>
+        /// Begin opening the drop down by firing cancelable events, opening the
+        /// drop-down or reverting, depending on the event argument values.
+        /// </summary>
+        /// <param name="oldValue">The original value, if needed for a revert.</param>
+        private void OpeningDropDown(bool oldValue)
+        {
+            var args = new CancelEventArgs();
+
+            // Opening
+            OnDropDownOpening(args);
+
+            if (args.Cancel)
+            {
+                _ignorePropertyChange = true;
+                SetValue(IsDropDownOpenProperty, oldValue);
+            }
+            else
+            {
+                OpenDropDown();
+            }
+
+            UpdatePseudoClasses();
+        }
+
+        /// <summary>
+        /// Connects to the DropDownPopup Closed event.
+        /// </summary>
+        /// <param name="sender">The source object.</param>
+        /// <param name="e">The event data.</param>
+        private void DropDownPopup_Closed(object sender, EventArgs e)
+        {
+            // Force the drop down dependency property to be false.
+            if (IsDropDownOpen)
+            {
+                IsDropDownOpen = false;
+            }
+
+            // Fire the DropDownClosed event
+            if (_popupHasOpened)
+            {
+                OnDropDownClosed(EventArgs.Empty);
+            }
+        }
+
+        /// <summary>
+        /// Handles the timer tick when using a populate delay.
+        /// </summary>
+        /// <param name="sender">The source object.</param>
+        /// <param name="e">The event arguments.</param>
+        private void PopulateDropDown(object sender, EventArgs e)
+        {
+            if (_delayTimer != null)
+            {
+                _delayTimer.Stop();
+            }
+
+            // Update the prefix/search text.
+            SearchText = Text;
+
+            if(TryPopulateAsync(SearchText))
+            {
+                return;
+            }
+
+            // The Populated event enables advanced, custom filtering. The
+            // client needs to directly update the ItemsSource collection or
+            // call the Populate method on the control to continue the
+            // display process if Cancel is set to true.
+            PopulatingEventArgs populating = new PopulatingEventArgs(SearchText);
+            OnPopulating(populating);
+            if (!populating.Cancel)
+            {
+                PopulateComplete();
+            }
+        }
+        private bool TryPopulateAsync(string searchText)
+        {
+            _populationCancellationTokenSource?.Cancel(false);
+            _populationCancellationTokenSource?.Dispose();
+            _populationCancellationTokenSource = null;
+
+            if(_asyncPopulator == null)
+            {
+                return false;
+            }
+
+            _populationCancellationTokenSource = new CancellationTokenSource();
+            var task = PopulateAsync(searchText, _populationCancellationTokenSource.Token);
+            if (task.Status == TaskStatus.Created)
+                task.Start();
+
+            return true;
+        }
+        private async Task PopulateAsync(string searchText, CancellationToken cancellationToken)
+        {
+
+            try
+            {
+                IEnumerable<object> result = await _asyncPopulator.Invoke(searchText, cancellationToken);
+                var resultList = result.ToList();
+
+                if (cancellationToken.IsCancellationRequested)
+                {
+                    return;
+                }
+
+                await Dispatcher.UIThread.InvokeAsync(() =>
+                {
+                    if (!cancellationToken.IsCancellationRequested)
+                    {
+                        Items = resultList;
+                        PopulateComplete();
+                    }
+                });
+            }
+            catch (TaskCanceledException)
+            { }
+            finally
+            {
+                _populationCancellationTokenSource?.Dispose();
+                _populationCancellationTokenSource = null;
+            }
+
+        }
+
+        /// <summary>
+        /// Private method that directly opens the popup, checks the expander
+        /// button, and then fires the Opened event.
+        /// </summary>
+        private void OpenDropDown()
+        {
+            if (DropDownPopup != null)
+            {
+                DropDownPopup.IsOpen = true;
+            }
+            _popupHasOpened = true;
+            OnDropDownOpened(EventArgs.Empty);
+        }
+
+        /// <summary>
+        /// Private method that directly closes the popup, flips the Checked
+        /// value, and then fires the Closed event.
+        /// </summary>
+        private void CloseDropDown()
+        {
+            if (_popupHasOpened)
+            {
+                if (SelectionAdapter != null)
+                {
+                    SelectionAdapter.SelectedItem = null;
+                }
+                if (DropDownPopup != null)
+                {
+                    DropDownPopup.IsOpen = false;
+                }
+                OnDropDownClosed(EventArgs.Empty);
+            }
+        }
+
+        /// <summary>
+        /// Formats an Item for text comparisons based on Converter
+        /// and ConverterCulture properties.
+        /// </summary>
+        /// <param name="value">The object to format.</param>
+        /// <param name="clearDataContext">A value indicating whether to clear
+        /// the data context after the lookup is performed.</param>
+        /// <returns>Formatted Value.</returns>
+        private string FormatValue(object value, bool clearDataContext)
+        {
+            string result = FormatValue(value);
+            if(clearDataContext && _valueBindingEvaluator != null)
+            {
+                _valueBindingEvaluator.ClearDataContext();
+            }
+
+            return result;
+        }
+
+        /// <summary>
+        /// Converts the specified object to a string by using the
+        /// <see cref="P:Avalonia.Data.Binding.Converter" /> and
+        /// <see cref="P:Avalonia.Data.Binding.ConverterCulture" /> values
+        /// of the binding object specified by the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ValueMemberBinding" />
+        /// property.
+        /// </summary>
+        /// <param name="value">The object to format as a string.</param>
+        /// <returns>The string representation of the specified object.</returns>
+        /// <remarks>
+        /// Override this method to provide a custom string conversion.
+        /// </remarks>
+        protected virtual string FormatValue(object value)
+        {
+            if (_valueBindingEvaluator != null)
+            {
+                return _valueBindingEvaluator.GetDynamicValue(value) ?? String.Empty;
+            }
+
+            if (_valueMemberSelector != null)
+            {
+                value = _valueMemberSelector.Select(value);
+            }
+
+            return value == null ? String.Empty : value.ToString();
+        }
+
+        /// <summary>
+        /// Handle the TextChanged event that is directly attached to the
+        /// TextBox part. This ensures that only user initiated actions will
+        /// result in an AutoCompleteBox suggestion and operation.
+        /// </summary>
+        private void OnTextBoxTextChanged()
+        {
+            //Uses Dispatcher.Post to allow the TextBox selection to update before processing
+            Dispatcher.UIThread.Post(() =>
+            {
+                // Call the central updated text method as a user-initiated action
+                TextUpdated(_textBox.Text, true);
+            });
+        }
+
+        /// <summary>
+        /// Updates both the text box value and underlying text dependency
+        /// property value if and when they change. Automatically fires the
+        /// text changed events when there is a change.
+        /// </summary>
+        /// <param name="value">The new string value.</param>
+        private void UpdateTextValue(string value)
+        {
+            UpdateTextValue(value, null);
+        }
+
+        /// <summary>
+        /// Updates both the text box value and underlying text dependency
+        /// property value if and when they change. Automatically fires the
+        /// text changed events when there is a change.
+        /// </summary>
+        /// <param name="value">The new string value.</param>
+        /// <param name="userInitiated">A nullable bool value indicating whether
+        /// the action was user initiated. In a user initiated mode, the
+        /// underlying text dependency property is updated. In a non-user
+        /// interaction, the text box value is updated. When user initiated is
+        /// null, all values are updated.</param>
+        private void UpdateTextValue(string value, bool? userInitiated)
+        {
+            bool callTextChanged = false;
+            // Update the Text dependency property
+            if ((userInitiated == null || userInitiated == true) && Text != value)
+            {
+                _ignoreTextPropertyChange++;
+                Text = value;
+                callTextChanged = true;
+            }
+
+            // Update the TextBox's Text dependency property
+            if ((userInitiated == null || userInitiated == false) && TextBox != null && TextBox.Text != value)
+            {
+                _ignoreTextPropertyChange++;
+                TextBox.Text = value ?? string.Empty;
+
+                // Text dependency property value was set, fire event
+                if (!callTextChanged && (Text == value || Text == null))
+                {
+                    callTextChanged = true;
+                }
+            }
+
+            if (callTextChanged)
+            {
+                OnTextChanged(new RoutedEventArgs());
+            }
+        }
+
+        /// <summary>
+        /// Handle the update of the text for the control from any source,
+        /// including the TextBox part and the Text dependency property.
+        /// </summary>
+        /// <param name="newText">The new text.</param>
+        /// <param name="userInitiated">A value indicating whether the update
+        /// is a user-initiated action. This should be a True value when the
+        /// TextUpdated method is called from a TextBox event handler.</param>
+        private void TextUpdated(string newText, bool userInitiated)
+        {
+            // Only process this event if it is coming from someone outside
+            // setting the Text dependency property directly.
+            if (_ignoreTextPropertyChange > 0)
+            {
+                _ignoreTextPropertyChange--;
+                return;
+            }
+
+            if (newText == null)
+            {
+                newText = string.Empty;
+            }
+
+            // The TextBox.TextChanged event was not firing immediately and
+            // was causing an immediate update, even with wrapping. If there is
+            // a selection currently, no update should happen.
+            if (IsTextCompletionEnabled && TextBox != null && TextBoxSelectionLength > 0 && TextBoxSelectionStart != TextBox.Text.Length)
+            {
+                return;
+            }
+
+            // Evaluate the conditions needed for completion.
+            // 1. Minimum prefix length
+            // 2. If a delay timer is in use, use it
+            bool populateReady = newText.Length >= MinimumPrefixLength && MinimumPrefixLength >= 0;
+            _userCalledPopulate = populateReady ? userInitiated : false;
+
+            // Update the interface and values only as necessary
+            UpdateTextValue(newText, userInitiated);
+
+            if (populateReady)
+            {
+                _ignoreTextSelectionChange = true;
+
+                if (_delayTimer != null)
+                {
+                    _delayTimer.Start();
+                }
+                else
+                {
+                    PopulateDropDown(this, EventArgs.Empty);
+                }
+            }
+            else
+            {
+                SearchText = string.Empty;
+                if (SelectedItem != null)
+                {
+                    _skipSelectedItemTextUpdate = true;
+                }
+                SelectedItem = null;
+                if (IsDropDownOpen)
+                {
+                    IsDropDownOpen = false;
+                }
+            }
+        }
+
+        /// <summary>
+        /// A simple helper method to clear the view and ensure that a view
+        /// object is always present and not null.
+        /// </summary>
+        private void ClearView()
+        {
+            if (_view == null)
+            {
+                _view = new AvaloniaList<object>();
+            }
+            else
+            {
+                _view.Clear();
+            }
+        }
+
+        /// <summary>
+        /// Walks through the items enumeration. Performance is not going to be
+        /// perfect with the current implementation.
+        /// </summary>
+        private void RefreshView()
+        {
+            if (_items == null)
+            {
+                ClearView();
+                return;
+            }
+
+            // Cache the current text value
+            string text = Text ?? string.Empty;
+
+            // Determine if any filtering mode is on
+            bool stringFiltering = TextFilter != null;
+            bool objectFiltering = FilterMode == AutoCompleteFilterMode.Custom && TextFilter == null;
+
+            int view_index = 0;
+            int view_count = _view.Count;
+            List<object> items = _items;
+            foreach (object item in items)
+            {
+                bool inResults = !(stringFiltering || objectFiltering);
+                if (!inResults)
+                {
+                    inResults = stringFiltering ? TextFilter(text, FormatValue(item)) : ItemFilter(text, item);
+                }
+
+                if (view_count > view_index && inResults && _view[view_index] == item)
+                {
+                    // Item is still in the view
+                    view_index++;
+                }
+                else if (inResults)
+                {
+                    // Insert the item
+                    if (view_count > view_index && _view[view_index] != item)
+                    {
+                        // Replace item
+                        // Unfortunately replacing via index throws a fatal
+                        // exception: View[view_index] = item;
+                        // Cost: O(n) vs O(1)
+                        _view.RemoveAt(view_index);
+                        _view.Insert(view_index, item);
+                        view_index++;
+                    }
+                    else
+                    {
+                        // Add the item
+                        if (view_index == view_count)
+                        {
+                            // Constant time is preferred (Add).
+                            _view.Add(item);
+                        }
+                        else
+                        {
+                            _view.Insert(view_index, item);
+                        }
+                        view_index++;
+                        view_count++;
+                    }
+                }
+                else if (view_count > view_index && _view[view_index] == item)
+                {
+                    // Remove the item
+                    _view.RemoveAt(view_index);
+                    view_count--;
+                }
+            }
+
+            // Clear the evaluator to discard a reference to the last item
+            if (_valueBindingEvaluator != null)
+            {
+                _valueBindingEvaluator.ClearDataContext();
+            }
+        }
+
+        /// <summary>
+        /// Handle any change to the ItemsSource dependency property, update
+        /// the underlying ObservableCollection view, and set the selection
+        /// adapter's ItemsSource to the view if appropriate.
+        /// </summary>
+        /// <param name="newValue">The new enumerable reference.</param>
+        private void OnItemsChanged(IEnumerable newValue)
+        {
+            // Remove handler for oldValue.CollectionChanged (if present)
+            _collectionChangeSubscription?.Dispose();
+            _collectionChangeSubscription = null;
+
+            // Add handler for newValue.CollectionChanged (if possible)
+            if (newValue is INotifyCollectionChanged newValueINotifyCollectionChanged)
+            {
+                _collectionChangeSubscription = newValueINotifyCollectionChanged.WeakSubscribe(ItemsCollectionChanged);
+            }
+
+            // Store a local cached copy of the data
+            _items = newValue == null ? null : new List<object>(newValue.Cast<object>().ToList());
+
+            // Clear and set the view on the selection adapter
+            ClearView();
+            if (SelectionAdapter != null && SelectionAdapter.Items != _view)
+            {
+                SelectionAdapter.Items = _view;
+            }
+            if (IsDropDownOpen)
+            {
+                RefreshView();
+            }
+        }
+
+        /// <summary>
+        /// Method that handles the ObservableCollection.CollectionChanged event for the ItemsSource property.
+        /// </summary>
+        /// <param name="sender">The object that raised the event.</param>
+        /// <param name="e">The event data.</param>
+        private void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            // Update the cache
+            if (e.Action == NotifyCollectionChangedAction.Remove && e.OldItems != null)
+            {
+                for (int index = 0; index < e.OldItems.Count; index++)
+                {
+                    _items.RemoveAt(e.OldStartingIndex);
+                }
+            }
+            if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null && _items.Count >= e.NewStartingIndex)
+            {
+                for (int index = 0; index < e.NewItems.Count; index++)
+                {
+                    _items.Insert(e.NewStartingIndex + index, e.NewItems[index]);
+                }
+            }
+            if (e.Action == NotifyCollectionChangedAction.Replace && e.NewItems != null && e.OldItems != null)
+            {
+                for (int index = 0; index < e.NewItems.Count; index++)
+                {
+                    _items[e.NewStartingIndex] = e.NewItems[index];
+                }
+            }
+
+            // Update the view
+            if (e.Action == NotifyCollectionChangedAction.Remove || e.Action == NotifyCollectionChangedAction.Replace)
+            {
+                for (int index = 0; index < e.OldItems.Count; index++)
+                {
+                    _view.Remove(e.OldItems[index]);
+                }
+            }
+
+            if (e.Action == NotifyCollectionChangedAction.Reset)
+            {
+                // Significant changes to the underlying data.
+                ClearView();
+                if (Items != null)
+                {
+                    _items = new List<object>(Items.Cast<object>().ToList());
+                }
+            }
+
+            // Refresh the observable collection used in the selection adapter.
+            RefreshView();
+        }
+
+        /// <summary>
+        /// Notifies the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> that the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.Items" />
+        /// property has been set and the data can be filtered to provide
+        /// possible matches in the drop-down.
+        /// </summary>
+        /// <remarks>
+        /// Call this method when you are providing custom population of
+        /// the drop-down portion of the AutoCompleteBox, to signal the control
+        /// that you are done with the population process.
+        /// Typically, you use PopulateComplete when the population process
+        /// is a long-running process and you want to cancel built-in filtering
+        ///  of the ItemsSource items. In this case, you can handle the
+        /// Populated event and set PopulatingEventArgs.Cancel to true.
+        /// When the long-running process has completed you call
+        /// PopulateComplete to indicate the drop-down is populated.
+        /// </remarks>
+        public void PopulateComplete()
+        {
+            // Apply the search filter
+            RefreshView();
+
+            // Fire the Populated event containing the read-only view data.
+            PopulatedEventArgs populated = new PopulatedEventArgs(new ReadOnlyCollection<object>(_view));
+            OnPopulated(populated);
+
+            if (SelectionAdapter != null && SelectionAdapter.Items != _view)
+            {
+                SelectionAdapter.Items = _view;
+            }
+
+            bool isDropDownOpen = _userCalledPopulate && (_view.Count > 0);
+            if (isDropDownOpen != IsDropDownOpen)
+            {
+                _ignorePropertyChange = true;
+                IsDropDownOpen = isDropDownOpen;
+            }
+            if (IsDropDownOpen)
+            {
+                OpeningDropDown(false);
+            }
+            else
+            {
+                ClosingDropDown(true);
+            }
+
+            UpdateTextCompletion(_userCalledPopulate);
+        }
+
+        /// <summary>
+        /// Performs text completion, if enabled, and a lookup on the underlying
+        /// item values for an exact match. Will update the SelectedItem value.
+        /// </summary>
+        /// <param name="userInitiated">A value indicating whether the operation
+        /// was user initiated. Text completion will not be performed when not
+        /// directly initiated by the user.</param>
+        private void UpdateTextCompletion(bool userInitiated)
+        {
+            // By default this method will clear the selected value
+            object newSelectedItem = null;
+            string text = Text;
+
+            // Text search is StartsWith explicit and only when enabled, in
+            // line with WPF's ComboBox lookup. When in use it will associate
+            // a Value with the Text if it is found in ItemsSource. This is
+            // only valid when there is data and the user initiated the action.
+            if (_view.Count > 0)
+            {
+                if (IsTextCompletionEnabled && TextBox != null && userInitiated)
+                {
+                    int currentLength = TextBox.Text.Length;
+                    int selectionStart = TextBoxSelectionStart;
+                    if (selectionStart == text.Length && selectionStart > _textSelectionStart)
+                    {
+                        // When the FilterMode dependency property is set to
+                        // either StartsWith or StartsWithCaseSensitive, the
+                        // first item in the view is used. This will improve
+                        // performance on the lookup. It assumes that the
+                        // FilterMode the user has selected is an acceptable
+                        // case sensitive matching function for their scenario.
+                        object top = FilterMode == AutoCompleteFilterMode.StartsWith || FilterMode == AutoCompleteFilterMode.StartsWithCaseSensitive
+                            ? _view[0]
+                            : TryGetMatch(text, _view, AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith));
+
+                        // If the search was successful, update SelectedItem
+                        if (top != null)
+                        {
+                            newSelectedItem = top;
+                            string topString = FormatValue(top, true);
+
+                            // Only replace partially when the two words being the same
+                            int minLength = Math.Min(topString.Length, Text.Length);
+                            if (AutoCompleteSearch.Equals(Text.Substring(0, minLength), topString.Substring(0, minLength)))
+                            {
+                                // Update the text
+                                UpdateTextValue(topString);
+
+                                // Select the text past the user's caret
+                                TextBox.SelectionStart = currentLength;
+                                TextBox.SelectionEnd = topString.Length;
+                            }
+                        }
+                    }
+                }
+                else
+                {
+                    // Perform an exact string lookup for the text. This is a
+                    // design change from the original Toolkit release when the
+                    // IsTextCompletionEnabled property behaved just like the
+                    // WPF ComboBox's IsTextSearchEnabled property.
+                    //
+                    // This change provides the behavior that most people expect
+                    // to find: a lookup for the value is always performed.
+                    newSelectedItem = TryGetMatch(text, _view, AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive));
+                }
+            }
+
+            // Update the selected item property
+
+            if (SelectedItem != newSelectedItem)
+            {
+                _skipSelectedItemTextUpdate = true;
+            }
+            SelectedItem = newSelectedItem;
+
+            // Restore updates for TextSelection
+            if (_ignoreTextSelectionChange)
+            {
+                _ignoreTextSelectionChange = false;
+                if (TextBox != null)
+                {
+                    _textSelectionStart = TextBoxSelectionStart;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Attempts to look through the view and locate the specific exact
+        /// text match.
+        /// </summary>
+        /// <param name="searchText">The search text.</param>
+        /// <param name="view">The view reference.</param>
+        /// <param name="predicate">The predicate to use for the partial or
+        /// exact match.</param>
+        /// <returns>Returns the object or null.</returns>
+        private object TryGetMatch(string searchText, AvaloniaList<object> view, AutoCompleteFilterPredicate<string> predicate)
+        {
+            if (view != null && view.Count > 0)
+            {
+                foreach (object o in view)
+                {
+                    if (predicate(searchText, FormatValue(o)))
+                    {
+                        return o;
+                    }
+                }
+            }
+
+            return null;
+        }
+
+        private void UpdatePseudoClasses()
+        {
+            PseudoClasses.Set(":dropdownopen", IsDropDownOpen);
+        }
+
+        private void ClearTextBoxSelection()
+        {
+            if (TextBox != null)
+            {
+                int length = TextBox.Text?.Length ?? 0;
+                TextBox.SelectionStart = length;
+                TextBox.SelectionEnd = length;
+            }
+        }
+
+        /// <summary>
+        /// Called when the selected item is changed, updates the text value
+        /// that is displayed in the text box part.
+        /// </summary>
+        /// <param name="newItem">The new item.</param>
+        private void OnSelectedItemChanged(object newItem)
+        {
+            string text;
+
+            if (newItem == null)
+            {
+                text = SearchText;
+            }
+            else
+            {
+                text = FormatValue(newItem, true);
+            }
+
+            // Update the Text property and the TextBox values
+            UpdateTextValue(text);
+
+            // Move the caret to the end of the text box
+            ClearTextBoxSelection();
+        }
+
+        /// <summary>
+        /// Handles the SelectionChanged event of the selection adapter.
+        /// </summary>
+        /// <param name="sender">The source object.</param>
+        /// <param name="e">The selection changed event data.</param>
+        private void OnAdapterSelectionChanged(object sender, SelectionChangedEventArgs e)
+        {
+            SelectedItem = _adapter.SelectedItem;
+        }
+
+        //TODO Check UpdateTextCompletion
+        /// <summary>
+        /// Handles the Commit event on the selection adapter.
+        /// </summary>
+        /// <param name="sender">The source object.</param>
+        /// <param name="e">The event data.</param>
+        private void OnAdapterSelectionComplete(object sender, RoutedEventArgs e)
+        {
+            IsDropDownOpen = false;
+
+            // Completion will update the selected value
+            //UpdateTextCompletion(false);
+
+            // Text should not be selected
+            ClearTextBoxSelection();
+
+            TextBox.Focus();
+        }
+
+        /// <summary>
+        /// Handles the Cancel event on the selection adapter.
+        /// </summary>
+        /// <param name="sender">The source object.</param>
+        /// <param name="e">The event data.</param>
+        private void OnAdapterSelectionCanceled(object sender, RoutedEventArgs e)
+        {
+            UpdateTextValue(SearchText);
+
+            // Completion will update the selected value
+            UpdateTextCompletion(false);
+        }
+
+        /// <summary>
+        /// A predefined set of filter functions for the known, built-in
+        /// AutoCompleteFilterMode enumeration values.
+        /// </summary>
+        private static class AutoCompleteSearch
+        {
+            /// <summary>
+            /// Index function that retrieves the filter for the provided
+            /// AutoCompleteFilterMode.
+            /// </summary>
+            /// <param name="FilterMode">The built-in search mode.</param>
+            /// <returns>Returns the string-based comparison function.</returns>
+            public static AutoCompleteFilterPredicate<string> GetFilter(AutoCompleteFilterMode FilterMode)
+            {
+                switch (FilterMode)
+                {
+                    case AutoCompleteFilterMode.Contains:
+                        return Contains;
+
+                    case AutoCompleteFilterMode.ContainsCaseSensitive:
+                        return ContainsCaseSensitive;
+
+                    case AutoCompleteFilterMode.ContainsOrdinal:
+                        return ContainsOrdinal;
+
+                    case AutoCompleteFilterMode.ContainsOrdinalCaseSensitive:
+                        return ContainsOrdinalCaseSensitive;
+
+                    case AutoCompleteFilterMode.Equals:
+                        return Equals;
+
+                    case AutoCompleteFilterMode.EqualsCaseSensitive:
+                        return EqualsCaseSensitive;
+
+                    case AutoCompleteFilterMode.EqualsOrdinal:
+                        return EqualsOrdinal;
+
+                    case AutoCompleteFilterMode.EqualsOrdinalCaseSensitive:
+                        return EqualsOrdinalCaseSensitive;
+
+                    case AutoCompleteFilterMode.StartsWith:
+                        return StartsWith;
+
+                    case AutoCompleteFilterMode.StartsWithCaseSensitive:
+                        return StartsWithCaseSensitive;
+
+                    case AutoCompleteFilterMode.StartsWithOrdinal:
+                        return StartsWithOrdinal;
+
+                    case AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive:
+                        return StartsWithOrdinalCaseSensitive;
+
+                    case AutoCompleteFilterMode.None:
+                    case AutoCompleteFilterMode.Custom:
+                    default:
+                        return null;
+                }
+            }
+
+            /// <summary>
+            /// An implementation of the Contains member of string that takes in a
+            /// string comparison. The traditional .NET string Contains member uses
+            /// StringComparison.Ordinal.
+            /// </summary>
+            /// <param name="s">The string.</param>
+            /// <param name="value">The string value to search for.</param>
+            /// <param name="comparison">The string comparison type.</param>
+            /// <returns>Returns true when the substring is found.</returns>
+            private static bool Contains(string s, string value, StringComparison comparison)
+            {
+                return s.IndexOf(value, comparison) >= 0;
+            }
+
+            /// <summary>
+            /// Check if the string value begins with the text.
+            /// </summary>
+            /// <param name="text">The AutoCompleteBox prefix text.</param>
+            /// <param name="value">The item's string value.</param>
+            /// <returns>Returns true if the condition is met.</returns>
+            public static bool StartsWith(string text, string value)
+            {
+                return value.StartsWith(text, StringComparison.CurrentCultureIgnoreCase);
+            }
+
+            /// <summary>
+            /// Check if the string value begins with the text.
+            /// </summary>
+            /// <param name="text">The AutoCompleteBox prefix text.</param>
+            /// <param name="value">The item's string value.</param>
+            /// <returns>Returns true if the condition is met.</returns>
+            public static bool StartsWithCaseSensitive(string text, string value)
+            {
+                return value.StartsWith(text, StringComparison.CurrentCulture);
+            }
+
+            /// <summary>
+            /// Check if the string value begins with the text.
+            /// </summary>
+            /// <param name="text">The AutoCompleteBox prefix text.</param>
+            /// <param name="value">The item's string value.</param>
+            /// <returns>Returns true if the condition is met.</returns>
+            public static bool StartsWithOrdinal(string text, string value)
+            {
+                return value.StartsWith(text, StringComparison.OrdinalIgnoreCase);
+            }
+
+            /// <summary>
+            /// Check if the string value begins with the text.
+            /// </summary>
+            /// <param name="text">The AutoCompleteBox prefix text.</param>
+            /// <param name="value">The item's string value.</param>
+            /// <returns>Returns true if the condition is met.</returns>
+            public static bool StartsWithOrdinalCaseSensitive(string text, string value)
+            {
+                return value.StartsWith(text, StringComparison.Ordinal);
+            }
+
+            /// <summary>
+            /// Check if the prefix is contained in the string value. The current
+            /// culture's case insensitive string comparison operator is used.
+            /// </summary>
+            /// <param name="text">The AutoCompleteBox prefix text.</param>
+            /// <param name="value">The item's string value.</param>
+            /// <returns>Returns true if the condition is met.</returns>
+            public static bool Contains(string text, string value)
+            {
+                return Contains(value, text, StringComparison.CurrentCultureIgnoreCase);
+            }
+
+            /// <summary>
+            /// Check if the prefix is contained in the string value.
+            /// </summary>
+            /// <param name="text">The AutoCompleteBox prefix text.</param>
+            /// <param name="value">The item's string value.</param>
+            /// <returns>Returns true if the condition is met.</returns>
+            public static bool ContainsCaseSensitive(string text, string value)
+            {
+                return Contains(value, text, StringComparison.CurrentCulture);
+            }
+
+            /// <summary>
+            /// Check if the prefix is contained in the string value.
+            /// </summary>
+            /// <param name="text">The AutoCompleteBox prefix text.</param>
+            /// <param name="value">The item's string value.</param>
+            /// <returns>Returns true if the condition is met.</returns>
+            public static bool ContainsOrdinal(string text, string value)
+            {
+                return Contains(value, text, StringComparison.OrdinalIgnoreCase);
+            }
+
+            /// <summary>
+            /// Check if the prefix is contained in the string value.
+            /// </summary>
+            /// <param name="text">The AutoCompleteBox prefix text.</param>
+            /// <param name="value">The item's string value.</param>
+            /// <returns>Returns true if the condition is met.</returns>
+            public static bool ContainsOrdinalCaseSensitive(string text, string value)
+            {
+                return Contains(value, text, StringComparison.Ordinal);
+            }
+
+            /// <summary>
+            /// Check if the string values are equal.
+            /// </summary>
+            /// <param name="text">The AutoCompleteBox prefix text.</param>
+            /// <param name="value">The item's string value.</param>
+            /// <returns>Returns true if the condition is met.</returns>
+            public static bool Equals(string text, string value)
+            {
+                return value.Equals(text, StringComparison.CurrentCultureIgnoreCase);
+            }
+
+            /// <summary>
+            /// Check if the string values are equal.
+            /// </summary>
+            /// <param name="text">The AutoCompleteBox prefix text.</param>
+            /// <param name="value">The item's string value.</param>
+            /// <returns>Returns true if the condition is met.</returns>
+            public static bool EqualsCaseSensitive(string text, string value)
+            {
+                return value.Equals(text, StringComparison.CurrentCulture);
+            }
+
+            /// <summary>
+            /// Check if the string values are equal.
+            /// </summary>
+            /// <param name="text">The AutoCompleteBox prefix text.</param>
+            /// <param name="value">The item's string value.</param>
+            /// <returns>Returns true if the condition is met.</returns>
+            public static bool EqualsOrdinal(string text, string value)
+            {
+                return value.Equals(text, StringComparison.OrdinalIgnoreCase);
+            }
+
+            /// <summary>
+            /// Check if the string values are equal.
+            /// </summary>
+            /// <param name="text">The AutoCompleteBox prefix text.</param>
+            /// <param name="value">The item's string value.</param>
+            /// <returns>Returns true if the condition is met.</returns>
+            public static bool EqualsOrdinalCaseSensitive(string text, string value)
+            {
+                return value.Equals(text, StringComparison.Ordinal);
+            }
+        }
+
+        /// <summary>
+        /// A framework element that permits a binding to be evaluated in a new data
+        /// context leaf node.
+        /// </summary>
+        /// <typeparam name="T">The type of dynamic binding to return.</typeparam>
+        public class BindingEvaluator<T> : Control
+        {
+            /// <summary>
+            /// Gets or sets the string value binding used by the control.
+            /// </summary>
+            private IBinding _binding;
+
+            #region public T Value
+
+            /// <summary>
+            /// Identifies the Value dependency property.
+            /// </summary>
+            public static readonly StyledProperty<T> ValueProperty =
+                AvaloniaProperty.Register<BindingEvaluator<T>, T>(nameof(Value));
+
+            /// <summary>
+            /// Gets or sets the data item value.
+            /// </summary>
+            public T Value
+            {
+                get { return GetValue(ValueProperty); }
+                set { SetValue(ValueProperty, value); }
+            }
+
+            #endregion public string Value
+
+            /// <summary>
+            /// Gets or sets the value binding.
+            /// </summary>
+            public IBinding ValueBinding
+            {
+                get { return _binding; }
+                set
+                {
+                    _binding = value;
+                    AvaloniaObjectExtensions.Bind(this, ValueProperty, value);
+                }
+            }
+
+            /// <summary>
+            /// Initializes a new instance of the BindingEvaluator class.
+            /// </summary>
+            public BindingEvaluator()
+            { }
+
+            /// <summary>
+            /// Initializes a new instance of the BindingEvaluator class,
+            /// setting the initial binding to the provided parameter.
+            /// </summary>
+            /// <param name="binding">The initial string value binding.</param>
+            public BindingEvaluator(IBinding binding)
+                : this()
+            {
+                ValueBinding = binding;
+            }
+
+            /// <summary>
+            /// Clears the data context so that the control does not keep a
+            /// reference to the last-looked up item.
+            /// </summary>
+            public void ClearDataContext()
+            {
+                DataContext = null;
+            }
+
+            /// <summary>
+            /// Updates the data context of the framework element and returns the
+            /// updated binding value.
+            /// </summary>
+            /// <param name="o">The object to use as the data context.</param>
+            /// <param name="clearDataContext">If set to true, this parameter will
+            /// clear the data context immediately after retrieving the value.</param>
+            /// <returns>Returns the evaluated T value of the bound dependency
+            /// property.</returns>
+            public T GetDynamicValue(object o, bool clearDataContext)
+            {
+                DataContext = o;
+                T value = Value;
+                if (clearDataContext)
+                {
+                    DataContext = null;
+                }
+                return value;
+            }
+
+            /// <summary>
+            /// Updates the data context of the framework element and returns the
+            /// updated binding value.
+            /// </summary>
+            /// <param name="o">The object to use as the data context.</param>
+            /// <returns>Returns the evaluated T value of the bound dependency
+            /// property.</returns>
+            public T GetDynamicValue(object o)
+            {
+                DataContext = o;
+                return Value;
+            }
+        }
+    }
+}

+ 19 - 29
src/Avalonia.Controls/Border.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 Avalonia;
+using Avalonia.Controls.Utils;
 using Avalonia.Media;
 
 namespace Avalonia.Controls
@@ -8,7 +10,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// A control which decorates a child with a border and background.
     /// </summary>
-    public class Border : Decorator
+    public partial class Border : Decorator
     {
         /// <summary>
         /// Defines the <see cref="Background"/> property.
@@ -25,14 +27,16 @@ namespace Avalonia.Controls
         /// <summary>
         /// Defines the <see cref="BorderThickness"/> property.
         /// </summary>
-        public static readonly StyledProperty<double> BorderThicknessProperty =
-            AvaloniaProperty.Register<Border, double>(nameof(BorderThickness));
+        public static readonly StyledProperty<Thickness> BorderThicknessProperty =
+            AvaloniaProperty.Register<Border, Thickness>(nameof(BorderThickness));
 
         /// <summary>
         /// Defines the <see cref="CornerRadius"/> property.
         /// </summary>
-        public static readonly StyledProperty<float> CornerRadiusProperty =
-            AvaloniaProperty.Register<Border, float>(nameof(CornerRadius));
+        public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
+            AvaloniaProperty.Register<Border, CornerRadius>(nameof(CornerRadius));
+
+        private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper();
 
         /// <summary>
         /// Initializes static members of the <see cref="Border"/> class.
@@ -63,7 +67,7 @@ namespace Avalonia.Controls
         /// <summary>
         /// Gets or sets the thickness of the border.
         /// </summary>
-        public double BorderThickness
+        public Thickness BorderThickness
         {
             get { return GetValue(BorderThicknessProperty); }
             set { SetValue(BorderThicknessProperty, value); }
@@ -72,7 +76,7 @@ namespace Avalonia.Controls
         /// <summary>
         /// Gets or sets the radius of the border rounded corners.
         /// </summary>
-        public float CornerRadius
+        public CornerRadius CornerRadius
         {
             get { return GetValue(CornerRadiusProperty); }
             set { SetValue(CornerRadiusProperty, value); }
@@ -84,21 +88,7 @@ namespace Avalonia.Controls
         /// <param name="context">The drawing context.</param>
         public override void Render(DrawingContext context)
         {
-            var background = Background;
-            var borderBrush = BorderBrush;
-            var borderThickness = BorderThickness;
-            var cornerRadius = CornerRadius;
-            var rect = new Rect(Bounds.Size).Deflate(BorderThickness);
-
-            if (background != null)
-            {
-                context.FillRectangle(background, rect, cornerRadius);
-            }
-
-            if (borderBrush != null && borderThickness > 0)
-            {
-                context.DrawRectangle(new Pen(borderBrush, borderThickness), rect, cornerRadius);
-            }
+            _borderRenderHelper.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush);
         }
 
         /// <summary>
@@ -120,10 +110,12 @@ namespace Avalonia.Controls
         {
             if (Child != null)
             {
-                var padding = Padding + new Thickness(BorderThickness);
+                var padding = Padding + BorderThickness;
                 Child.Arrange(new Rect(finalSize).Deflate(padding));
             }
 
+            _borderRenderHelper.Update(finalSize, BorderThickness, CornerRadius);           
+
             return finalSize;
         }
 
@@ -131,19 +123,17 @@ namespace Avalonia.Controls
             Size availableSize,
             IControl child,
             Thickness padding,
-            double borderThickness)
+            Thickness borderThickness)
         {
-            padding += new Thickness(borderThickness);
+            padding += borderThickness;
 
             if (child != null)
             {
                 child.Measure(availableSize.Deflate(padding));
                 return child.DesiredSize.Inflate(padding);
             }
-            else
-            {
-                return new Size(padding.Left + padding.Right, padding.Bottom + padding.Top);
-            }
+
+            return new Size(padding.Left + padding.Right, padding.Bottom + padding.Top);
         }
     }
 }

+ 5 - 0
src/Avalonia.Controls/ButtonSpinner.cs

@@ -201,6 +201,11 @@ namespace Avalonia.Controls
             }
         }
 
+        protected override void OnValidSpinDirectionChanged(ValidSpinDirections oldValue, ValidSpinDirections newValue)
+        {
+            SetButtonUsage();
+        }
+
         /// <summary>
         /// Called when the <see cref="AllowSpin"/> property value changed.
         /// </summary>

+ 35 - 0
src/Avalonia.Controls/MenuItem.cs

@@ -93,6 +93,7 @@ namespace Avalonia.Controls
         static MenuItem()
         {
             SelectableMixin.Attach<MenuItem>(IsSelectedProperty);
+            CommandProperty.Changed.Subscribe(CommandChanged);
             FocusableProperty.OverrideDefaultValue<MenuItem>(true);
             IconProperty.Changed.AddClassHandler<MenuItem>(x => x.IconChanged);
             ItemsPanelProperty.OverrideDefaultValue<MenuItem>(DefaultPanel);
@@ -424,6 +425,40 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <summary>
+        /// Called when the <see cref="Command"/> property changes.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void CommandChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is MenuItem menuItem)
+            {
+                if (e.OldValue is ICommand oldCommand)
+                {
+                    oldCommand.CanExecuteChanged -= menuItem.CanExecuteChanged;
+                }
+
+                if (e.NewValue is ICommand newCommand)
+                {
+                    newCommand.CanExecuteChanged += menuItem.CanExecuteChanged;
+                }
+
+                menuItem.CanExecuteChanged(menuItem, EventArgs.Empty);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="ICommand.CanExecuteChanged"/> event fires.
+        /// </summary>
+        /// <param name="sender">The event sender.</param>
+        /// <param name="e">The event args.</param>
+        private void CanExecuteChanged(object sender, EventArgs e)
+        {
+            // HACK: Just set the IsEnabled property for the moment. This needs to be changed to
+            // use IsEnabledCore etc. but it will do for now.
+            IsEnabled = Command == null || Command.CanExecute(CommandParameter);
+        }
+
         /// <summary>
         /// Called when the <see cref="Icon"/> property changes.
         /// </summary>

+ 998 - 0
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@@ -0,0 +1,998 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using Avalonia.Controls.Primitives;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Threading;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Control that represents a TextBox with button spinners that allow incrementing and decrementing numeric values.
+    /// </summary>
+    public class NumericUpDown : TemplatedControl
+    {
+        /// <summary>
+        /// Defines the <see cref="AllowSpin"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> AllowSpinProperty =
+            ButtonSpinner.AllowSpinProperty.AddOwner<NumericUpDown>();
+
+        /// <summary>
+        /// Defines the <see cref="ButtonSpinnerLocation"/> property.
+        /// </summary>
+        public static readonly StyledProperty<Location> ButtonSpinnerLocationProperty =
+            ButtonSpinner.ButtonSpinnerLocationProperty.AddOwner<NumericUpDown>();
+
+        /// <summary>
+        /// Defines the <see cref="ShowButtonSpinner"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> ShowButtonSpinnerProperty =
+            ButtonSpinner.ShowButtonSpinnerProperty.AddOwner<NumericUpDown>();
+
+        /// <summary>
+        /// Defines the <see cref="ClipValueToMinMax"/> property.
+        /// </summary>
+        public static readonly DirectProperty<NumericUpDown, bool> ClipValueToMinMaxProperty =
+            AvaloniaProperty.RegisterDirect<NumericUpDown, bool>(nameof(ClipValueToMinMax),
+                updown => updown.ClipValueToMinMax, (updown, b) => updown.ClipValueToMinMax = b);
+
+        /// <summary>
+        /// Defines the <see cref="CultureInfo"/> property.
+        /// </summary>
+        public static readonly DirectProperty<NumericUpDown, CultureInfo> CultureInfoProperty =
+            AvaloniaProperty.RegisterDirect<NumericUpDown, CultureInfo>(nameof(CultureInfo), o => o.CultureInfo,
+                (o, v) => o.CultureInfo = v, CultureInfo.CurrentCulture);
+
+        /// <summary>
+        /// Defines the <see cref="FormatString"/> property.
+        /// </summary>
+        public static readonly StyledProperty<string> FormatStringProperty =
+            AvaloniaProperty.Register<NumericUpDown, string>(nameof(FormatString), string.Empty);
+
+        /// <summary>
+        /// Defines the <see cref="Increment"/> property.
+        /// </summary>
+        public static readonly StyledProperty<double> IncrementProperty =
+            AvaloniaProperty.Register<NumericUpDown, double>(nameof(Increment), 1.0d, validate: OnCoerceIncrement);
+
+        /// <summary>
+        /// Defines the <see cref="IsReadOnly"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> IsReadOnlyProperty =
+            AvaloniaProperty.Register<NumericUpDown, bool>(nameof(IsReadOnly));
+
+        /// <summary>
+        /// Defines the <see cref="Maximum"/> property.
+        /// </summary>
+        public static readonly StyledProperty<double> MaximumProperty =
+            AvaloniaProperty.Register<NumericUpDown, double>(nameof(Maximum), double.MaxValue, validate: OnCoerceMaximum);
+
+        /// <summary>
+        /// Defines the <see cref="Minimum"/> property.
+        /// </summary>
+        public static readonly StyledProperty<double> MinimumProperty =
+            AvaloniaProperty.Register<NumericUpDown, double>(nameof(Minimum), double.MinValue, validate: OnCoerceMinimum);
+
+        /// <summary>
+        /// Defines the <see cref="ParsingNumberStyle"/> property.
+        /// </summary>
+        public static readonly DirectProperty<NumericUpDown, NumberStyles> ParsingNumberStyleProperty =
+            AvaloniaProperty.RegisterDirect<NumericUpDown, NumberStyles>(nameof(ParsingNumberStyle),
+                updown => updown.ParsingNumberStyle, (updown, style) => updown.ParsingNumberStyle = style);
+
+        /// <summary>
+        /// Defines the <see cref="Text"/> property.
+        /// </summary>
+        public static readonly DirectProperty<NumericUpDown, string> TextProperty =
+            AvaloniaProperty.RegisterDirect<NumericUpDown, string>(nameof(Text), o => o.Text, (o, v) => o.Text = v,
+                defaultBindingMode: BindingMode.TwoWay);
+
+        /// <summary>
+        /// Defines the <see cref="Value"/> property.
+        /// </summary>
+        public static readonly DirectProperty<NumericUpDown, double> ValueProperty =
+            AvaloniaProperty.RegisterDirect<NumericUpDown, double>(nameof(Value), updown => updown.Value,
+                (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay);
+
+        /// <summary>
+        /// Defines the <see cref="Watermark"/> property.
+        /// </summary>
+        public static readonly StyledProperty<string> WatermarkProperty =
+            AvaloniaProperty.Register<NumericUpDown, string>(nameof(Watermark));
+
+        private IDisposable _textBoxTextChangedSubscription;
+
+        private double _value;
+        private string _text;
+        private bool _internalValueSet;
+        private bool _clipValueToMinMax;
+        private bool _isSyncingTextAndValueProperties;
+        private bool _isTextChangedFromUI;
+        private CultureInfo _cultureInfo;
+        private NumberStyles _parsingNumberStyle = NumberStyles.Any;
+        
+        /// <summary>
+        /// Gets the Spinner template part.
+        /// </summary>
+        private Spinner Spinner { get; set; }
+
+        /// <summary>
+        /// Gets the TextBox template part.
+        /// </summary>
+        private TextBox TextBox { get; set; }
+
+        /// <summary>
+        /// Gets or sets the ability to perform increment/decrement operations via the keyboard, button spinners, or mouse wheel.
+        /// </summary>
+        public bool AllowSpin
+        {
+            get { return GetValue(AllowSpinProperty); }
+            set { SetValue(AllowSpinProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets current location of the <see cref="ButtonSpinner"/>.
+        /// </summary>
+        public Location ButtonSpinnerLocation
+        {
+            get { return GetValue(ButtonSpinnerLocationProperty); }
+            set { SetValue(ButtonSpinnerLocationProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the spin buttons should be shown.
+        /// </summary>
+        public bool ShowButtonSpinner
+        {
+            get { return GetValue(ShowButtonSpinnerProperty); }
+            set { SetValue(ShowButtonSpinnerProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets if the value should be clipped when minimum/maximum is reached.
+        /// </summary>
+        public bool ClipValueToMinMax
+        {
+            get { return _clipValueToMinMax; }
+            set { SetAndRaise(ClipValueToMinMaxProperty, ref _clipValueToMinMax, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the current CultureInfo.
+        /// </summary>
+        public CultureInfo CultureInfo
+        {
+            get { return _cultureInfo; }
+            set { SetAndRaise(CultureInfoProperty, ref _cultureInfo, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the display format of the <see cref="Value"/>.
+        /// </summary>
+        public string FormatString
+        {
+            get { return GetValue(FormatStringProperty); }
+            set { SetValue(FormatStringProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the amount in which to increment the <see cref="Value"/>.
+        /// </summary>
+        public double Increment
+        {
+            get { return GetValue(IncrementProperty); }
+            set { SetValue(IncrementProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets if the control is read only.
+        /// </summary>
+        public bool IsReadOnly
+        {
+            get { return GetValue(IsReadOnlyProperty); }
+            set { SetValue(IsReadOnlyProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the maximum allowed value.
+        /// </summary>
+        public double Maximum
+        {
+            get { return GetValue(MaximumProperty); }
+            set { SetValue(MaximumProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the minimum allowed value.
+        /// </summary>
+        public double Minimum
+        {
+            get { return GetValue(MinimumProperty); }
+            set { SetValue(MinimumProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the parsing style (AllowLeadingWhite, Float, AllowHexSpecifier, ...). By default, Any.
+        /// </summary>
+        public NumberStyles ParsingNumberStyle
+        {
+            get { return _parsingNumberStyle; }
+            set { SetAndRaise(ParsingNumberStyleProperty, ref _parsingNumberStyle, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the formatted string representation of the value.
+        /// </summary>
+        public string Text
+        {
+            get { return _text; }
+            set { SetAndRaise(TextProperty, ref _text, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the value.
+        /// </summary>
+        public double Value
+        {
+            get { return _value; }
+            set
+            {
+                value = OnCoerceValue(value);
+                SetAndRaise(ValueProperty, ref _value, value);
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the object to use as a watermark if the <see cref="Value"/> is null.
+        /// </summary>
+        public string Watermark
+        {
+            get { return GetValue(WatermarkProperty); }
+            set { SetValue(WatermarkProperty, value); }
+        }
+
+        /// <summary>
+        /// Initializes new instance of <see cref="NumericUpDown"/> class.
+        /// </summary>
+        public NumericUpDown()
+        {
+            Initialized += (sender, e) =>
+            {
+                if (!_internalValueSet && IsInitialized)
+                {
+                    SyncTextAndValueProperties(false, null, true);
+                }
+
+                SetValidSpinDirection();
+            };
+        }
+
+        /// <summary>
+        /// Initializes static members of the <see cref="NumericUpDown"/> class.
+        /// </summary>
+        static NumericUpDown()
+        {
+            CultureInfoProperty.Changed.Subscribe(OnCultureInfoChanged);
+            FormatStringProperty.Changed.Subscribe(FormatStringChanged);
+            IncrementProperty.Changed.Subscribe(IncrementChanged);
+            IsReadOnlyProperty.Changed.Subscribe(OnIsReadOnlyChanged);
+            MaximumProperty.Changed.Subscribe(OnMaximumChanged);
+            MinimumProperty.Changed.Subscribe(OnMinimumChanged);
+            TextProperty.Changed.Subscribe(OnTextChanged);
+            ValueProperty.Changed.Subscribe(OnValueChanged);
+        }
+
+        /// <inheritdoc />
+        protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
+        {
+            if (TextBox != null)
+            {
+                TextBox.PointerPressed -= TextBoxOnPointerPressed;
+                _textBoxTextChangedSubscription?.Dispose();
+            }
+            TextBox = e.NameScope.Find<TextBox>("PART_TextBox");
+            if (TextBox != null)
+            {
+                TextBox.Text = Text;
+                TextBox.PointerPressed += TextBoxOnPointerPressed;
+                _textBoxTextChangedSubscription = TextBox.GetObservable(TextBox.TextProperty).Subscribe(txt => TextBoxOnTextChanged());
+            }
+
+            if (Spinner != null)
+            {
+                Spinner.Spin -= OnSpinnerSpin;
+            }
+
+            Spinner = e.NameScope.Find<Spinner>("PART_Spinner");
+
+            if (Spinner != null)
+            {
+                Spinner.Spin += OnSpinnerSpin;
+            }
+
+            SetValidSpinDirection();
+        }
+
+        /// <inheritdoc />
+        protected override void OnKeyDown(KeyEventArgs e)
+        {
+            switch (e.Key)
+            {
+                case Key.Enter:
+                    var commitSuccess = CommitInput();
+                    e.Handled = !commitSuccess;
+                    break;
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="CultureInfo"/> property value changed.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnCultureInfoChanged(CultureInfo oldValue, CultureInfo newValue)
+        {
+            if (IsInitialized)
+            {
+                SyncTextAndValueProperties(false, null);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="FormatString"/> property value changed.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnFormatStringChanged(string oldValue, string newValue)
+        {
+            if (IsInitialized)
+            {
+                SyncTextAndValueProperties(false, null);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Increment"/> property value changed.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnIncrementChanged(double oldValue, double newValue)
+        {
+            if (IsInitialized)
+            {
+                SetValidSpinDirection();
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="IsReadOnly"/> property value changed.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnIsReadOnlyChanged(bool oldValue, bool newValue)
+        {
+            SetValidSpinDirection();
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Maximum"/> property value changed.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnMaximumChanged(double oldValue, double newValue)
+        {
+            if (IsInitialized)
+            {
+                SetValidSpinDirection();
+            }
+            if (ClipValueToMinMax)
+            {
+                Value = MathUtilities.Clamp(Value, Minimum, Maximum);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Minimum"/> property value changed.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnMinimumChanged(double oldValue, double newValue)
+        {
+            if (IsInitialized)
+            {
+                SetValidSpinDirection();
+            }
+            if (ClipValueToMinMax)
+            {
+                Value = MathUtilities.Clamp(Value, Minimum, Maximum);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Text"/> property value changed.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnTextChanged(string oldValue, string newValue)
+        {
+            if (IsInitialized)
+            {
+                SyncTextAndValueProperties(true, Text);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Value"/> property value changed.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnValueChanged(double oldValue, double newValue)
+        {
+            if (!_internalValueSet && IsInitialized)
+            {
+                SyncTextAndValueProperties(false, null, true);
+            }
+
+            SetValidSpinDirection();
+
+            RaiseValueChangedEvent(oldValue, newValue);
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Increment"/> property has to be coerced.
+        /// </summary>
+        /// <param name="baseValue">The value.</param>
+        protected virtual double OnCoerceIncrement(double baseValue)
+        {
+            return baseValue;
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Maximum"/> property has to be coerced.
+        /// </summary>
+        /// <param name="baseValue">The value.</param>
+        protected virtual double OnCoerceMaximum(double baseValue)
+        {
+            return Math.Max(baseValue, Minimum);
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Minimum"/> property has to be coerced.
+        /// </summary>
+        /// <param name="baseValue">The value.</param>
+        protected virtual double OnCoerceMinimum(double baseValue)
+        {
+            return Math.Min(baseValue, Maximum);
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Value"/> property has to be coerced.
+        /// </summary>
+        /// <param name="baseValue">The value.</param>
+        protected virtual double OnCoerceValue(double baseValue)
+        {
+            return baseValue;
+        }
+
+        /// <summary>
+        /// Raises the OnSpin event when spinning is initiated by the end-user.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        protected virtual void OnSpin(SpinEventArgs e)
+        {
+            if (e == null)
+            {
+                throw new ArgumentNullException("e");
+            }
+
+            var handler = Spinned;
+            handler?.Invoke(this, e);
+
+            if (e.Direction == SpinDirection.Increase)
+            {
+                DoIncrement();
+            }
+            else
+            {
+                DoDecrement();
+            }
+        }
+
+        /// <summary>
+        /// Raises the <see cref="ValueChanged"/> event.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void RaiseValueChangedEvent(double oldValue, double newValue)
+        {
+            var e = new NumericUpDownValueChangedEventArgs(ValueChangedEvent, oldValue, newValue);
+            RaiseEvent(e);
+        }
+
+        /// <summary>
+        /// Converts the formatted text to a value.
+        /// </summary>
+        private double ConvertTextToValue(string text)
+        {
+            double result = 0;
+
+            if (string.IsNullOrEmpty(text))
+            {
+                return result;
+            }
+
+            // Since the conversion from Value to text using a FormartString may not be parsable,
+            // we verify that the already existing text is not the exact same value.
+            var currentValueText = ConvertValueToText();
+            if (Equals(currentValueText, text))
+            {
+                return Value;
+            }
+
+            result = ConvertTextToValueCore(currentValueText, text);
+
+            if (ClipValueToMinMax)
+            {
+                return MathUtilities.Clamp(result, Minimum, Maximum);
+            }
+
+            ValidateMinMax(result);
+
+            return result;
+        }
+
+        /// <summary>
+        /// Converts the value to formatted text.
+        /// </summary>
+        /// <returns></returns>
+        private string ConvertValueToText()
+        {
+            //Manage FormatString of type "{}{0:N2} °" (in xaml) or "{0:N2} °" in code-behind.
+            if (FormatString.Contains("{0"))
+            {
+                return string.Format(CultureInfo, FormatString, Value);
+            }
+
+            return Value.ToString(FormatString, CultureInfo);
+        }
+
+        /// <summary>
+        /// Called by OnSpin when the spin direction is SpinDirection.Increase.
+        /// </summary>
+        private void OnIncrement()
+        {
+            var result = Value + Increment;
+            Value = MathUtilities.Clamp(result, Minimum, Maximum);
+        }
+
+        /// <summary>
+        /// Called by OnSpin when the spin direction is SpinDirection.Descrease.
+        /// </summary>
+        private void OnDecrement()
+        {
+            var result = Value - Increment;
+            Value = MathUtilities.Clamp(result, Minimum, Maximum);
+        }
+
+        /// <summary>
+        /// Sets the valid spin directions.
+        /// </summary>
+        private void SetValidSpinDirection()
+        {
+            var validDirections = ValidSpinDirections.None;
+
+            // Zero increment always prevents spin.
+            if (Increment != 0 && !IsReadOnly)
+            {
+                if (Value < Maximum)
+                {
+                    validDirections = validDirections | ValidSpinDirections.Increase;
+                }
+
+                if (Value > Minimum)
+                {
+                    validDirections = validDirections | ValidSpinDirections.Decrease;
+                }
+            }
+
+            if (Spinner != null)
+            {
+                Spinner.ValidSpinDirection = validDirections;
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="CultureInfo"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void OnCultureInfoChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is NumericUpDown upDown)
+            {
+                var oldValue = (CultureInfo)e.OldValue;
+                var newValue = (CultureInfo)e.NewValue;
+                upDown.OnCultureInfoChanged(oldValue, newValue);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Increment"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void IncrementChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is NumericUpDown upDown)
+            {
+                var oldValue = (double)e.OldValue;
+                var newValue = (double)e.NewValue;
+                upDown.OnIncrementChanged(oldValue, newValue);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="FormatString"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void FormatStringChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is NumericUpDown upDown)
+            {
+                var oldValue = (string)e.OldValue;
+                var newValue = (string)e.NewValue;
+                upDown.OnFormatStringChanged(oldValue, newValue);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="IsReadOnly"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void OnIsReadOnlyChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is NumericUpDown upDown)
+            {
+                var oldValue = (bool)e.OldValue;
+                var newValue = (bool)e.NewValue;
+                upDown.OnIsReadOnlyChanged(oldValue, newValue);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Maximum"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void OnMaximumChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is NumericUpDown upDown)
+            {
+                var oldValue = (double)e.OldValue;
+                var newValue = (double)e.NewValue;
+                upDown.OnMaximumChanged(oldValue, newValue);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Minimum"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void OnMinimumChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is NumericUpDown upDown)
+            {
+                var oldValue = (double)e.OldValue;
+                var newValue = (double)e.NewValue;
+                upDown.OnMinimumChanged(oldValue, newValue);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Text"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void OnTextChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is NumericUpDown upDown)
+            {
+                var oldValue = (string)e.OldValue;
+                var newValue = (string)e.NewValue;
+                upDown.OnTextChanged(oldValue, newValue);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Value"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void OnValueChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is NumericUpDown upDown)
+            {
+                var oldValue = (double)e.OldValue;
+                var newValue = (double)e.NewValue;
+                upDown.OnValueChanged(oldValue, newValue);
+            }
+        }
+
+        private void SetValueInternal(double value)
+        {
+            _internalValueSet = true;
+            try
+            {
+                Value = value;
+            }
+            finally
+            {
+                _internalValueSet = false;
+            }
+        }
+
+        private static double OnCoerceMaximum(NumericUpDown upDown, double value)
+        {
+            return upDown.OnCoerceMaximum(value);
+        }
+
+        private static double OnCoerceMinimum(NumericUpDown upDown, double value)
+        {
+            return upDown.OnCoerceMinimum(value);
+        }
+
+        private static double OnCoerceIncrement(NumericUpDown upDown, double value)
+        {
+            return upDown.OnCoerceIncrement(value);
+        }
+
+        private void TextBoxOnTextChanged()
+        {
+            try
+            {
+                _isTextChangedFromUI = true;
+                if (TextBox != null)
+                {
+                    Text = TextBox.Text;
+                }
+            }
+            finally
+            {
+                _isTextChangedFromUI = false;
+            }
+        }
+
+        private void OnSpinnerSpin(object sender, SpinEventArgs e)
+        {
+            if (AllowSpin && !IsReadOnly)
+            {
+                var spin = !e.UsingMouseWheel;
+                spin |= ((TextBox != null) && TextBox.IsFocused);
+
+                if (spin)
+                {
+                    e.Handled = true;
+                    OnSpin(e);
+                }
+            }
+        }
+
+        private void DoDecrement()
+        {
+            if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Decrease) == ValidSpinDirections.Decrease)
+            {
+                OnDecrement();
+            }
+        }
+
+        private void DoIncrement()
+        {
+            if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Increase) == ValidSpinDirections.Increase)
+            {
+                OnIncrement();
+            }
+        }
+
+        public event EventHandler<SpinEventArgs> Spinned;
+
+        private void TextBoxOnPointerPressed(object sender, PointerPressedEventArgs e)
+        {
+            if (e.Device.Captured != Spinner)
+            {
+                Dispatcher.UIThread.InvokeAsync(() => { e.Device.Capture(Spinner); }, DispatcherPriority.Input);
+            }
+        }
+
+        /// <summary>
+        /// Defines the <see cref="ValueChanged"/> event.
+        /// </summary>
+        public static readonly RoutedEvent<NumericUpDownValueChangedEventArgs> ValueChangedEvent =
+            RoutedEvent.Register<NumericUpDown, NumericUpDownValueChangedEventArgs>(nameof(ValueChanged), RoutingStrategies.Bubble);
+
+        /// <summary>
+        /// Raised when the <see cref="Value"/> changes.
+        /// </summary>
+        public event EventHandler<SpinEventArgs> ValueChanged
+        {
+            add { AddHandler(ValueChangedEvent, value); }
+            remove { RemoveHandler(ValueChangedEvent, value); }
+        }
+
+        private bool CommitInput()
+        {
+            return SyncTextAndValueProperties(true, Text);
+        }
+
+        /// <summary>
+        /// Synchronize <see cref="Text"/> and <see cref="Value"/> properties.
+        /// </summary>
+        /// <param name="updateValueFromText">If value should be updated from text.</param>
+        /// <param name="text">The text.</param>
+        private bool SyncTextAndValueProperties(bool updateValueFromText, string text)
+        {
+            return SyncTextAndValueProperties(updateValueFromText, text, false);
+        }
+
+        /// <summary>
+        /// Synchronize <see cref="Text"/> and <see cref="Value"/> properties.
+        /// </summary>
+        /// <param name="updateValueFromText">If value should be updated from text.</param>
+        /// <param name="text">The text.</param>
+        /// <param name="forceTextUpdate">Force text update.</param>
+        private bool SyncTextAndValueProperties(bool updateValueFromText, string text, bool forceTextUpdate)
+        {
+            if (_isSyncingTextAndValueProperties)
+                return true;
+
+            _isSyncingTextAndValueProperties = true;
+            var parsedTextIsValid = true;
+            try
+            {
+                if (updateValueFromText)
+                {
+                    if (!string.IsNullOrEmpty(text))
+                    {
+                        try
+                        {
+                            var newValue = ConvertTextToValue(text);
+                            if (!Equals(newValue, Value))
+                            {
+                                SetValueInternal(newValue);
+                            }
+                        }
+                        catch
+                        {
+                            parsedTextIsValid = false;
+                        }
+                    }
+                }
+
+                // Do not touch the ongoing text input from user.
+                if (!_isTextChangedFromUI)
+                {
+                    var keepEmpty = !forceTextUpdate && string.IsNullOrEmpty(Text);
+                    if (!keepEmpty)
+                    {
+                        var newText = ConvertValueToText();
+                        if (!Equals(Text, newText))
+                        {
+                            Text = newText;
+                        }
+                    }
+
+                    // Sync Text and textBox
+                    if (TextBox != null)
+                    {
+                        TextBox.Text = Text;
+                    }
+                }
+
+                if (_isTextChangedFromUI && !parsedTextIsValid)
+                {
+                    // Text input was made from the user and the text
+                    // repesents an invalid value. Disable the spinner in this case.
+                    if (Spinner != null)
+                    {
+                        Spinner.ValidSpinDirection = ValidSpinDirections.None;
+                    }
+                }
+                else
+                {
+                    SetValidSpinDirection();
+                }
+            }
+            finally
+            {
+                _isSyncingTextAndValueProperties = false;
+            }
+            return parsedTextIsValid;
+        }
+
+        private double ConvertTextToValueCore(string currentValueText, string text)
+        {
+            double result;
+
+            if (IsPercent(FormatString))
+            {
+                result = decimal.ToDouble(ParsePercent(text, CultureInfo));
+            }
+            else
+            {
+                // Problem while converting new text
+                if (!double.TryParse(text, ParsingNumberStyle, CultureInfo, out var outputValue))
+                {
+                    var shouldThrow = true;
+
+                    // Check if CurrentValueText is also failing => it also contains special characters. ex : 90°
+                    if (!double.TryParse(currentValueText, ParsingNumberStyle, CultureInfo, out var _))
+                    {
+                        // extract non-digit characters
+                        var currentValueTextSpecialCharacters = currentValueText.Where(c => !char.IsDigit(c));
+                        var textSpecialCharacters = text.Where(c => !char.IsDigit(c));
+                        // same non-digit characters on currentValueText and new text => remove them on new Text to parse it again.
+                        if (currentValueTextSpecialCharacters.Except(textSpecialCharacters).ToList().Count == 0)
+                        {
+                            foreach (var character in textSpecialCharacters)
+                            {
+                                text = text.Replace(character.ToString(), string.Empty);
+                            }
+                            // if without the special characters, parsing is good, do not throw
+                            if (double.TryParse(text, ParsingNumberStyle, CultureInfo, out outputValue))
+                            {
+                                shouldThrow = false;
+                            }
+                        }
+                    }
+
+                    if (shouldThrow)
+                    {
+                        throw new InvalidDataException("Input string was not in a correct format.");
+                    }
+                }
+                result = outputValue;
+            }
+            return result;
+        }
+
+        private void ValidateMinMax(double value)
+        {
+            if (value < Minimum)
+            {
+                throw new ArgumentOutOfRangeException(nameof(Minimum), string.Format("Value must be greater than Minimum value of {0}", Minimum));
+            }
+            else if (value > Maximum)
+            {
+                throw new ArgumentOutOfRangeException(nameof(Maximum), string.Format("Value must be less than Maximum value of {0}", Maximum));
+            }
+        }
+
+        /// <summary>
+        /// Parse percent format text
+        /// </summary>
+        /// <param name="text">Text to parse.</param>
+        /// <param name="cultureInfo">The culture info.</param>
+        private static decimal ParsePercent(string text, IFormatProvider cultureInfo)
+        {
+            var info = NumberFormatInfo.GetInstance(cultureInfo);
+            text = text.Replace(info.PercentSymbol, null);
+            var result = decimal.Parse(text, NumberStyles.Any, info);
+            result = result / 100;
+            return result;
+        }
+
+
+        private bool IsPercent(string stringToTest)
+        {
+            var PIndex = stringToTest.IndexOf("P", StringComparison.Ordinal);
+            if (PIndex >= 0)
+            {
+                //stringToTest contains a "P" between 2 "'", it's considered as text, not percent
+                var isText = stringToTest.Substring(0, PIndex).Contains("'")
+                             && stringToTest.Substring(PIndex, FormatString.Length - PIndex).Contains("'");
+
+                return !isText;
+            }
+            return false;
+        }
+    }
+}

+ 16 - 0
src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs

@@ -0,0 +1,16 @@
+using Avalonia.Interactivity;
+
+namespace Avalonia.Controls
+{
+    public class NumericUpDownValueChangedEventArgs : RoutedEventArgs
+    {
+        public NumericUpDownValueChangedEventArgs(RoutedEvent routedEvent, double oldValue,  double newValue) : base(routedEvent)
+        {
+            OldValue = oldValue;
+            NewValue = newValue;
+        }
+
+        public double OldValue { get; }
+        public double NewValue { get; }
+    }
+}

+ 70 - 83
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@@ -4,6 +4,7 @@
 using System;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
+using Avalonia.Controls.Utils;
 using Avalonia.Layout;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
@@ -31,7 +32,7 @@ namespace Avalonia.Controls.Presenters
         /// <summary>
         /// Defines the <see cref="BorderThickness"/> property.
         /// </summary>
-        public static readonly StyledProperty<double> BorderThicknessProperty =
+        public static readonly StyledProperty<Thickness> BorderThicknessProperty =
             Border.BorderThicknessProperty.AddOwner<ContentPresenter>();
 
         /// <summary>
@@ -57,7 +58,7 @@ namespace Avalonia.Controls.Presenters
         /// <summary>
         /// Defines the <see cref="CornerRadius"/> property.
         /// </summary>
-        public static readonly StyledProperty<float> CornerRadiusProperty =
+        public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
             Border.CornerRadiusProperty.AddOwner<ContentPresenter>();
 
         /// <summary>
@@ -76,11 +77,12 @@ namespace Avalonia.Controls.Presenters
         /// Defines the <see cref="Padding"/> property.
         /// </summary>
         public static readonly StyledProperty<Thickness> PaddingProperty =
-            Border.PaddingProperty.AddOwner<ContentPresenter>();
+            Decorator.PaddingProperty.AddOwner<ContentPresenter>();
 
         private IControl _child;
         private bool _createdChild;
         private IDataTemplate _dataTemplate;
+        private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper();
 
         /// <summary>
         /// Initializes static members of the <see cref="ContentPresenter"/> class.
@@ -120,7 +122,7 @@ namespace Avalonia.Controls.Presenters
         /// <summary>
         /// Gets or sets the thickness of the border.
         /// </summary>
-        public double BorderThickness
+        public Thickness BorderThickness
         {
             get { return GetValue(BorderThicknessProperty); }
             set { SetValue(BorderThicknessProperty, value); }
@@ -157,7 +159,7 @@ namespace Avalonia.Controls.Presenters
         /// <summary>
         /// Gets or sets the radius of the border rounded corners.
         /// </summary>
-        public float CornerRadius
+        public CornerRadius CornerRadius
         {
             get { return GetValue(CornerRadiusProperty); }
             set { SetValue(CornerRadiusProperty, value); }
@@ -221,7 +223,7 @@ namespace Avalonia.Controls.Presenters
         {
             var content = Content;
             var oldChild = Child;
-            var newChild = CreateChild();            
+            var newChild = CreateChild();
 
             // Remove the old child if we're not recycling it.
             if (oldChild != null && newChild != oldChild)
@@ -277,21 +279,7 @@ namespace Avalonia.Controls.Presenters
         /// <inheritdoc/>
         public override void Render(DrawingContext context)
         {
-            var background = Background;
-            var borderBrush = BorderBrush;
-            var borderThickness = BorderThickness;
-            var cornerRadius = CornerRadius;
-            var rect = new Rect(Bounds.Size).Deflate(BorderThickness);
-
-            if (background != null)
-            {
-                context.FillRectangle(background, rect, cornerRadius);
-            }
-
-            if (borderBrush != null && borderThickness > 0)
-            {
-                context.DrawRectangle(new Pen(borderBrush, borderThickness), rect, cornerRadius);
-            }
+            _borderRenderer.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush);
         }
 
         /// <summary>
@@ -344,7 +332,11 @@ namespace Avalonia.Controls.Presenters
         /// <inheritdoc/>
         protected override Size ArrangeOverride(Size finalSize)
         {
-            return ArrangeOverrideImpl(finalSize, new Vector());
+            finalSize = ArrangeOverrideImpl(finalSize, new Vector());
+
+            _borderRenderer.Update(finalSize, BorderThickness, CornerRadius);
+
+            return finalSize;
         }
 
         /// <summary>
@@ -372,74 +364,69 @@ namespace Avalonia.Controls.Presenters
 
         internal Size ArrangeOverrideImpl(Size finalSize, Vector offset)
         {
-            if (Child != null)
-            {
-                var padding = Padding;
-                var borderThickness = BorderThickness;
-                var horizontalContentAlignment = HorizontalContentAlignment;
-                var verticalContentAlignment = VerticalContentAlignment;
-                var useLayoutRounding = UseLayoutRounding;
-                var availableSizeMinusMargins = new Size(
-                    Math.Max(0, finalSize.Width - padding.Left - padding.Right - borderThickness),
-                    Math.Max(0, finalSize.Height - padding.Top - padding.Bottom - borderThickness));
-                var size = availableSizeMinusMargins;
-                var scale = GetLayoutScale();
-                var originX = offset.X + padding.Left + borderThickness;
-                var originY = offset.Y + padding.Top + borderThickness;
-
-                if (horizontalContentAlignment != HorizontalAlignment.Stretch)
-                {
-                    size = size.WithWidth(Math.Min(size.Width, DesiredSize.Width - padding.Left - padding.Right));
-                }
+            if (Child == null) return finalSize;
 
-                if (verticalContentAlignment != VerticalAlignment.Stretch)
-                {
-                    size = size.WithHeight(Math.Min(size.Height, DesiredSize.Height - padding.Top - padding.Bottom));
-                }
-
-                size = LayoutHelper.ApplyLayoutConstraints(Child, size);
-
-                if (useLayoutRounding)
-                {
-                    size = new Size(
-                        Math.Ceiling(size.Width * scale) / scale,
-                        Math.Ceiling(size.Height * scale) / scale);
-                    availableSizeMinusMargins = new Size(
-                        Math.Ceiling(availableSizeMinusMargins.Width * scale) / scale,
-                        Math.Ceiling(availableSizeMinusMargins.Height * scale) / scale);
-                }
+            var padding = Padding;
+            var borderThickness = BorderThickness;
+            var horizontalContentAlignment = HorizontalContentAlignment;
+            var verticalContentAlignment = VerticalContentAlignment;
+            var useLayoutRounding = UseLayoutRounding;
+            var availableSizeMinusMargins = new Size(
+                Math.Max(0, finalSize.Width - padding.Left - padding.Right - borderThickness.Left - borderThickness.Right),
+                Math.Max(0, finalSize.Height - padding.Top - padding.Bottom - borderThickness.Top - borderThickness.Bottom));
+            var size = availableSizeMinusMargins;
+            var scale = GetLayoutScale();
+            var originX = offset.X + padding.Left + borderThickness.Left;
+            var originY = offset.Y + padding.Top + borderThickness.Top;
+
+            if (horizontalContentAlignment != HorizontalAlignment.Stretch)
+            {
+                size = size.WithWidth(Math.Min(size.Width, DesiredSize.Width - padding.Left - padding.Right));
+            }
+            
+            if (verticalContentAlignment != VerticalAlignment.Stretch)
+            {
+                size = size.WithHeight(Math.Min(size.Height, DesiredSize.Height - padding.Top - padding.Bottom));
+            }
 
-                switch (horizontalContentAlignment)
-                {
-                    case HorizontalAlignment.Center:
-                    case HorizontalAlignment.Stretch:
-                        originX += (availableSizeMinusMargins.Width - size.Width) / 2;
-                        break;
-                    case HorizontalAlignment.Right:
-                        originX += availableSizeMinusMargins.Width - size.Width;
-                        break;
-                }
+            if (useLayoutRounding)
+            {
+                size = new Size(
+                    Math.Ceiling(size.Width * scale) / scale,
+                    Math.Ceiling(size.Height * scale) / scale);
+                availableSizeMinusMargins = new Size(
+                    Math.Ceiling(availableSizeMinusMargins.Width * scale) / scale,
+                    Math.Ceiling(availableSizeMinusMargins.Height * scale) / scale);
+            }
 
-                switch (verticalContentAlignment)
-                {
-                    case VerticalAlignment.Center:
-                    case VerticalAlignment.Stretch:
-                        originY += (availableSizeMinusMargins.Height - size.Height) / 2;
-                        break;
-                    case VerticalAlignment.Bottom:
-                        originY += availableSizeMinusMargins.Height - size.Height;
-                        break;
-                }
+            switch (horizontalContentAlignment)
+            {
+                case HorizontalAlignment.Center:
+                    originX += (availableSizeMinusMargins.Width - size.Width) / 2;
+                    break;
+                case HorizontalAlignment.Right:
+                    originX += availableSizeMinusMargins.Width - size.Width;
+                    break;
+            }
 
-                if (useLayoutRounding)
-                {
-                    originX = Math.Floor(originX * scale) / scale;
-                    originY = Math.Floor(originY * scale) / scale;
-                }
+            switch (verticalContentAlignment)
+            {
+                case VerticalAlignment.Center:
+                    originY += (availableSizeMinusMargins.Height - size.Height) / 2;
+                    break;
+                case VerticalAlignment.Bottom:
+                    originY += availableSizeMinusMargins.Height - size.Height;
+                    break;
+            }
 
-                Child.Arrange(new Rect(originX, originY, size.Width, size.Height));
+            if (useLayoutRounding)
+            {
+                originX = Math.Floor(originX * scale) / scale;
+                originY = Math.Floor(originY * scale) / scale;
             }
 
+            Child.Arrange(new Rect(originX, originY, size.Width, size.Height));
+
             return finalSize;
         }
 

+ 2 - 2
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@@ -32,7 +32,7 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Defines the <see cref="BorderThickness"/> property.
         /// </summary>
-        public static readonly StyledProperty<double> BorderThicknessProperty =
+        public static readonly StyledProperty<Thickness> BorderThicknessProperty =
             Border.BorderThicknessProperty.AddOwner<TemplatedControl>();
 
         /// <summary>
@@ -132,7 +132,7 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Gets or sets the thickness of the control's border.
         /// </summary>
-        public double BorderThickness
+        public Thickness BorderThickness
         {
             get { return GetValue(BorderThicknessProperty); }
             set { SetValue(BorderThicknessProperty, value); }

+ 25 - 4
src/Avalonia.Controls/TextBox.cs

@@ -85,6 +85,7 @@ namespace Avalonia.Controls
         private int _selectionEnd;
         private TextPresenter _presenter;
         private UndoRedoHelper<UndoRedoState> _undoRedoHelper;
+        private bool _isUndoingRedoing;
         private bool _ignoreTextChanges;
         private static readonly string[] invalidCharacters = new String[1]{"\u007f"};
 
@@ -198,7 +199,11 @@ namespace Avalonia.Controls
                 if (!_ignoreTextChanges)
                 {
                     CaretIndex = CoerceCaretIndex(CaretIndex, value?.Length ?? 0);
-                    SetAndRaise(TextProperty, ref _text, value);
+
+                    if (SetAndRaise(TextProperty, ref _text, value) && !_isUndoingRedoing)
+                    {
+                        _undoRedoHelper.Clear();
+                    }
                 }
             }
         }
@@ -367,14 +372,30 @@ namespace Avalonia.Controls
                 case Key.Z:
                     if (modifiers == InputModifiers.Control)
                     {
-                        _undoRedoHelper.Undo();
+                        try
+                        {
+                            _isUndoingRedoing = true;
+                            _undoRedoHelper.Undo();
+                        }
+                        finally
+                        {
+                            _isUndoingRedoing = false;
+                        }
                         handled = true;
                     }
                     break;
                 case Key.Y:
                     if (modifiers == InputModifiers.Control)
                     {
-                        _undoRedoHelper.Redo();
+                        try
+                        {
+                            _isUndoingRedoing = true;
+                            _undoRedoHelper.Redo();
+                        }
+                        finally
+                        {
+                            _isUndoingRedoing = false;
+                        }
                         handled = true;
                     }
                     break;
@@ -791,7 +812,7 @@ namespace Avalonia.Controls
             int pos = 0;
             int i;
 
-            for (i = 0; i < lines.Count; ++i)
+            for (i = 0; i < lines.Count - 1; ++i)
             {
                 var line = lines[i];
                 pos += line.Length;

+ 279 - 0
src/Avalonia.Controls/Utils/BorderRenderHelper.cs

@@ -0,0 +1,279 @@
+// 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.Media;
+
+namespace Avalonia.Controls.Utils
+{
+    internal class BorderRenderHelper
+    {
+        private bool _useComplexRendering;
+        private StreamGeometry _backgroundGeometryCache;
+        private StreamGeometry _borderGeometryCache;
+
+        public void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius)
+        {
+            if (borderThickness.IsUniform && cornerRadius.IsUniform)
+            {
+                _backgroundGeometryCache = null;
+                _borderGeometryCache = null;
+                _useComplexRendering = false;
+            }
+            else
+            {
+                _useComplexRendering = true;
+
+                var boundRect = new Rect(finalSize);
+                var innerRect = boundRect.Deflate(borderThickness);
+                var innerCoordinates = new BorderCoordinates(cornerRadius, borderThickness, false);
+
+                StreamGeometry backgroundGeometry = null;
+
+                if (innerRect.Width != 0 && innerRect.Height != 0)
+                {
+                    backgroundGeometry = new StreamGeometry();
+
+                    using (var ctx = backgroundGeometry.Open())
+                    {
+                        CreateGeometry(ctx, innerRect, innerCoordinates);
+                    }
+
+                    _backgroundGeometryCache = backgroundGeometry;
+                }
+                else
+                {
+                    _backgroundGeometryCache = null;
+                }
+
+                if (boundRect.Width != 0 && innerRect.Height != 0)
+                {
+                    var outerCoordinates = new BorderCoordinates(cornerRadius, borderThickness, true);
+                    var borderGeometry = new StreamGeometry();
+
+                    using (var ctx = borderGeometry.Open())
+                    {
+                        CreateGeometry(ctx, boundRect, outerCoordinates);
+
+                        if (backgroundGeometry != null)
+                        {
+                            CreateGeometry(ctx, innerRect, innerCoordinates);
+                        }
+                    }
+
+                    _borderGeometryCache = borderGeometry;
+                }
+                else
+                {
+                    _borderGeometryCache = null;
+                }
+            }
+        }
+
+        public void Render(DrawingContext context, Size size, Thickness borders, CornerRadius radii, IBrush background, IBrush borderBrush)
+        {
+            if (_useComplexRendering)
+            {
+                var backgroundGeometry = _backgroundGeometryCache;
+                if (backgroundGeometry != null)
+                {
+                    context.DrawGeometry(background, null, backgroundGeometry);
+                }
+
+                var borderGeometry = _borderGeometryCache;
+                if (borderGeometry != null)
+                {
+                    context.DrawGeometry(borderBrush, null, borderGeometry);
+                }
+            }
+            else
+            {
+                var borderThickness = borders.Left;
+                var cornerRadius = (float)radii.TopLeft;
+                var rect = new Rect(size);
+
+                if (background != null)
+                {
+                    context.FillRectangle(background, rect.Deflate(borders), cornerRadius);
+                }
+
+                if (borderBrush != null && borderThickness > 0)
+                {
+                    context.DrawRectangle(new Pen(borderBrush, borderThickness), rect.Deflate(borderThickness), cornerRadius);
+                }
+            }
+        }
+
+        private static void CreateGeometry(StreamGeometryContext context, Rect boundRect, BorderCoordinates borderCoordinates)
+        {
+            var topLeft = new Point(borderCoordinates.LeftTop, 0);
+            var topRight = new Point(boundRect.Width - borderCoordinates.RightTop, 0);
+            var rightTop = new Point(boundRect.Width, borderCoordinates.TopRight);
+            var rightBottom = new Point(boundRect.Width, boundRect.Height - borderCoordinates.BottomRight);
+            var bottomRight = new Point(boundRect.Width - borderCoordinates.RightBottom, boundRect.Height);
+            var bottomLeft = new Point(borderCoordinates.LeftBottom, boundRect.Height);
+            var leftBottom = new Point(0, boundRect.Height - borderCoordinates.BottomLeft);
+            var leftTop = new Point(0, borderCoordinates.TopLeft);
+
+
+            if (topLeft.X > topRight.X)
+            {
+                var scaledX = borderCoordinates.LeftTop / (borderCoordinates.LeftTop + borderCoordinates.RightTop) * boundRect.Width;
+                topLeft = new Point(scaledX, topLeft.Y);
+                topRight = new Point(scaledX, topRight.Y);
+            }
+
+            if (rightTop.Y > rightBottom.Y)
+            {
+                var scaledY = borderCoordinates.TopRight / (borderCoordinates.TopRight + borderCoordinates.BottomRight) * boundRect.Height;
+                rightTop = new Point(rightTop.X, scaledY);
+                rightBottom = new Point(rightBottom.X, scaledY);
+            }
+
+            if (bottomRight.X < bottomLeft.X)
+            {
+                var scaledX = borderCoordinates.LeftBottom / (borderCoordinates.LeftBottom + borderCoordinates.RightBottom) * boundRect.Width;
+                bottomRight = new Point(scaledX, bottomRight.Y);
+                bottomLeft = new Point(scaledX, bottomLeft.Y);
+            }
+
+            if (leftBottom.Y < leftTop.Y)
+            {
+                var scaledY = borderCoordinates.TopLeft / (borderCoordinates.TopLeft + borderCoordinates.BottomLeft) * boundRect.Height;
+                leftBottom = new Point(leftBottom.X, scaledY);
+                leftTop = new Point(leftTop.X, scaledY);
+            }
+
+            var offset = new Vector(boundRect.TopLeft.X, boundRect.TopLeft.Y);
+            topLeft += offset;
+            topRight += offset;
+            rightTop += offset;
+            rightBottom += offset;
+            bottomRight += offset;
+            bottomLeft += offset;
+            leftBottom += offset;
+            leftTop += offset;
+
+            context.BeginFigure(topLeft, true);
+
+            //Top
+            context.LineTo(topRight);
+
+            //TopRight corner
+            var radiusX = boundRect.TopRight.X - topRight.X;
+            var radiusY = rightTop.Y - boundRect.TopRight.Y;
+            if (radiusX != 0 || radiusY != 0)
+            {
+                context.ArcTo(rightTop, new Size(radiusY, radiusY), 0, false, SweepDirection.Clockwise);
+            }
+
+            //Right
+            context.LineTo(rightBottom);
+
+            //BottomRight corner
+            radiusX = boundRect.BottomRight.X - bottomRight.X;
+            radiusY = boundRect.BottomRight.Y - rightBottom.Y;
+            if (radiusX != 0 || radiusY != 0)
+            {
+                context.ArcTo(bottomRight, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise);
+            }
+
+            //Bottom
+            context.LineTo(bottomLeft);
+
+            //BottomLeft corner
+            radiusX = bottomLeft.X - boundRect.BottomLeft.X;
+            radiusY = boundRect.BottomLeft.Y - leftBottom.Y;
+            if (radiusX != 0 || radiusY != 0)
+            {
+                context.ArcTo(leftBottom, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise);
+            }
+
+            //Left
+            context.LineTo(leftTop);
+
+            //TopLeft corner
+            radiusX = topLeft.X - boundRect.TopLeft.X;
+            radiusY = leftTop.Y - boundRect.TopLeft.Y;
+
+            if (radiusX != 0 || radiusY != 0)
+            {
+                context.ArcTo(topLeft, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise);
+            }
+
+            context.EndFigure(true);
+        }
+
+        private struct BorderCoordinates
+        {
+            internal BorderCoordinates(CornerRadius cornerRadius, Thickness borderThickness, bool isOuter)
+            {
+                var left = 0.5 * borderThickness.Left;
+                var top = 0.5 * borderThickness.Top;
+                var right = 0.5 * borderThickness.Right;
+                var bottom = 0.5 * borderThickness.Bottom;
+
+                if (isOuter)
+                {
+                    if (cornerRadius.TopLeft == 0)
+                    {
+                        LeftTop = TopLeft = 0.0;
+                    }
+                    else
+                    {
+                        LeftTop = cornerRadius.TopLeft + left;
+                        TopLeft = cornerRadius.TopLeft + top;
+                    }
+                    if (cornerRadius.TopRight == 0)
+                    {
+                        TopRight = RightTop = 0;
+                    }
+                    else
+                    {
+                        TopRight = cornerRadius.TopRight + top;
+                        RightTop = cornerRadius.TopRight + right;
+                    }
+                    if (cornerRadius.BottomRight == 0)
+                    {
+                        RightBottom = BottomRight = 0;
+                    }
+                    else
+                    {
+                        RightBottom = cornerRadius.BottomRight + right;
+                        BottomRight = cornerRadius.BottomRight + bottom;
+                    }
+                    if (cornerRadius.BottomLeft == 0)
+                    {
+                        BottomLeft = LeftBottom = 0;
+                    }
+                    else
+                    {
+                        BottomLeft = cornerRadius.BottomLeft + bottom;
+                        LeftBottom = cornerRadius.BottomLeft + left;
+                    }
+                }
+                else
+                {
+                    LeftTop = Math.Max(0, cornerRadius.TopLeft - left);
+                    TopLeft = Math.Max(0, cornerRadius.TopLeft - top);
+                    TopRight = Math.Max(0, cornerRadius.TopRight - top);
+                    RightTop = Math.Max(0, cornerRadius.TopRight - right);
+                    RightBottom = Math.Max(0, cornerRadius.BottomRight - right);
+                    BottomRight = Math.Max(0, cornerRadius.BottomRight - bottom);
+                    BottomLeft = Math.Max(0, cornerRadius.BottomLeft - bottom);
+                    LeftBottom = Math.Max(0, cornerRadius.BottomLeft - left);
+                }
+            }
+
+            internal readonly double LeftTop;
+            internal readonly double TopLeft;
+            internal readonly double TopRight;
+            internal readonly double RightTop;
+            internal readonly double RightBottom;
+            internal readonly double BottomRight;
+            internal readonly double BottomLeft;
+            internal readonly double LeftBottom;
+        }
+
+    }
+}

+ 64 - 0
src/Avalonia.Controls/Utils/ISelectionAdapter.cs

@@ -0,0 +1,64 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+using System.Collections;
+using Avalonia.Interactivity;
+using Avalonia.Input;
+
+namespace Avalonia.Controls.Utils
+{
+    /// <summary>
+    /// Defines an item collection, selection members, and key handling for the
+    /// selection adapter contained in the drop-down portion of an
+    /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.
+    /// </summary>
+    public interface ISelectionAdapter
+    {
+        /// <summary>
+        /// Gets or sets the selected item.
+        /// </summary>
+        /// <value>The currently selected item.</value>
+        object SelectedItem { get; set; }
+
+        /// <summary>
+        /// Occurs when the
+        /// <see cref="P:Avalonia.Controls.Utils.ISelectionAdapter.SelectedItem" />
+        /// property value changes.
+        /// </summary>
+        event EventHandler<SelectionChangedEventArgs> SelectionChanged;
+        
+        /// <summary>
+        /// Gets or sets a collection that is used to generate content for the
+        /// selection adapter.
+        /// </summary>
+        /// <value>The collection that is used to generate content for the
+        /// selection adapter.</value>
+        IEnumerable Items { get; set; }
+
+        /// <summary>
+        /// Occurs when a selected item is not cancelled and is committed as the
+        /// selected item.
+        /// </summary>
+        event EventHandler<RoutedEventArgs> Commit;
+
+        /// <summary>
+        /// Occurs when a selection has been canceled.
+        /// </summary>
+        event EventHandler<RoutedEventArgs> Cancel;
+
+        /// <summary>
+        /// Provides handling for the
+        /// <see cref="E:Avalonia.Input.InputElement.KeyDown" /> event that occurs
+        /// when a key is pressed while the drop-down portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> has focus.
+        /// </summary>
+        /// <param name="e">A <see cref="T:Avalonia.Input.KeyEventArgs" />
+        /// that contains data about the
+        /// <see cref="E:Avalonia.Input.InputElement.KeyDown" /> event.</param>
+        void HandleKeyDown(KeyEventArgs e);
+    }
+
+}

+ 342 - 0
src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs

@@ -0,0 +1,342 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Controls.Primitives;
+using Avalonia.Interactivity;
+using Avalonia.Input;
+using Avalonia.LogicalTree;
+using System.Collections;
+using System.Diagnostics;
+
+namespace Avalonia.Controls.Utils
+{
+    /// <summary>
+    /// Represents the selection adapter contained in the drop-down portion of
+    /// an <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.
+    /// </summary>
+    public class SelectingItemsControlSelectionAdapter : ISelectionAdapter
+    {
+        /// <summary>
+        /// The SelectingItemsControl instance.
+        /// </summary>
+        private SelectingItemsControl _selector;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the selection change event 
+        /// should not be fired.
+        /// </summary>
+        private bool IgnoringSelectionChanged { get; set; }
+
+        /// <summary>
+        /// Gets or sets the underlying
+        /// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
+        /// control.
+        /// </summary>
+        /// <value>The underlying
+        /// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
+        /// control.</value>
+        public SelectingItemsControl SelectorControl
+        {
+            get { return _selector; }
+
+            set
+            {
+                if (_selector != null)
+                {
+                    _selector.SelectionChanged -= OnSelectionChanged;
+                    _selector.PointerReleased -= OnSelectorPointerReleased;
+                }
+
+                _selector = value;
+
+                if (_selector != null)
+                {
+                    _selector.SelectionChanged += OnSelectionChanged;
+                    _selector.PointerReleased += OnSelectorPointerReleased;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Occurs when the
+        /// <see cref="P:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter.SelectedItem" />
+        /// property value changes.
+        /// </summary>
+        public event EventHandler<SelectionChangedEventArgs> SelectionChanged;
+
+        /// <summary>
+        /// Occurs when an item is selected and is committed to the underlying
+        /// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
+        /// control.
+        /// </summary>
+        public event EventHandler<RoutedEventArgs> Commit;
+
+        /// <summary>
+        /// Occurs when a selection is canceled before it is committed.
+        /// </summary>
+        public event EventHandler<RoutedEventArgs> Cancel;
+
+        /// <summary>
+        /// Initializes a new instance of the
+        /// <see cref="T:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter" />
+        /// class.
+        /// </summary>
+        public SelectingItemsControlSelectionAdapter()
+        {
+
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the
+        /// <see cref="T:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapterr" />
+        /// class with the specified
+        /// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
+        /// control.
+        /// </summary>
+        /// <param name="selector">The
+        /// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" /> control
+        /// to wrap as a
+        /// <see cref="T:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter" />.</param>
+        public SelectingItemsControlSelectionAdapter(SelectingItemsControl selector)
+        {
+            SelectorControl = selector;
+        }
+
+        /// <summary>
+        /// Gets or sets the selected item of the selection adapter.
+        /// </summary>
+        /// <value>The selected item of the underlying selection adapter.</value>
+        public object SelectedItem
+        {
+            get
+            {
+                return SelectorControl?.SelectedItem;
+            }
+
+            set
+            {
+                IgnoringSelectionChanged = true;
+                if (SelectorControl != null)
+                {
+                    SelectorControl.SelectedItem = value;
+                }
+
+                // Attempt to reset the scroll viewer's position
+                if (value == null)
+                {
+                    ResetScrollViewer();
+                }
+
+                IgnoringSelectionChanged = false;
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets a collection that is used to generate the content of
+        /// the selection adapter.
+        /// </summary>
+        /// <value>The collection used to generate content for the selection
+        /// adapter.</value>
+        public IEnumerable Items
+        {
+            get
+            {
+                return SelectorControl?.Items;
+            }
+            set
+            {
+                if (SelectorControl != null)
+                {
+                    SelectorControl.Items = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// If the control contains a ScrollViewer, this will reset the viewer 
+        /// to be scrolled to the top.
+        /// </summary>
+        private void ResetScrollViewer()
+        {
+            if (SelectorControl != null)
+            {
+                ScrollViewer sv = SelectorControl.GetLogicalDescendants().OfType<ScrollViewer>().FirstOrDefault();
+                if (sv != null)
+                {
+                    sv.Offset = new Vector(0, 0);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Handles the mouse left button up event on the selector control.
+        /// </summary>
+        /// <param name="sender">The source object.</param>
+        /// <param name="e">The event data.</param>
+        private void OnSelectorPointerReleased(object sender, PointerReleasedEventArgs e)
+        {
+            if (e.MouseButton == MouseButton.Left)
+            {
+                OnCommit();
+            }
+        }
+
+        /// <summary>
+        /// Handles the SelectionChanged event on the SelectingItemsControl control.
+        /// </summary>
+        /// <param name="sender">The source object.</param>
+        /// <param name="e">The selection changed event data.</param>
+        private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
+        {
+            if (IgnoringSelectionChanged)
+            {
+                return;
+            }
+
+            SelectionChanged?.Invoke(sender, e);
+        }
+
+        /// <summary>
+        /// Increments the
+        /// <see cref="P:Avalonia.Controls.Primitives.SelectingItemsControl.SelectedIndex" />
+        /// property of the underlying
+        /// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
+        /// control.
+        /// </summary>
+        protected void SelectedIndexIncrement()
+        {
+            if (SelectorControl != null)
+            {
+                SelectorControl.SelectedIndex = SelectorControl.SelectedIndex + 1 >= SelectorControl.ItemCount ? -1 : SelectorControl.SelectedIndex + 1;
+            }
+        }
+
+        /// <summary>
+        /// Decrements the
+        /// <see cref="P:Avalonia.Controls.Primitives.SelectingItemsControl.SelectedIndex" />
+        /// property of the underlying
+        /// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
+        /// control.
+        /// </summary>
+        protected void SelectedIndexDecrement()
+        {
+            if (SelectorControl != null)
+            {
+                int index = SelectorControl.SelectedIndex;
+                if (index >= 0)
+                {
+                    SelectorControl.SelectedIndex--;
+                }
+                else if (index == -1)
+                {
+                    SelectorControl.SelectedIndex = SelectorControl.ItemCount - 1;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Provides handling for the
+        /// <see cref="E:Avalonia.Input.InputElement.KeyDown" /> event that occurs
+        /// when a key is pressed while the drop-down portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> has focus.
+        /// </summary>
+        /// <param name="e">A <see cref="T:Avalonia.Input.KeyEventArgs" />
+        /// that contains data about the
+        /// <see cref="E:Avalonia.Input.InputElement.KeyDown" /> event.</param>
+        public void HandleKeyDown(KeyEventArgs e)
+        {
+            switch (e.Key)
+            {
+                case Key.Enter:
+                    OnCommit();
+                    e.Handled = true;
+                    break;
+
+                case Key.Up:
+                    SelectedIndexDecrement();
+                    e.Handled = true;
+                    break;
+
+                case Key.Down:
+                    if ((e.Modifiers & InputModifiers.Alt) == InputModifiers.None)
+                    {
+                        SelectedIndexIncrement();
+                        e.Handled = true;
+                    }
+                    break;
+
+                case Key.Escape:
+                    OnCancel();
+                    e.Handled = true;
+                    break;
+
+                default:
+                    break;
+            }
+        }
+
+        /// <summary>
+        /// Raises the
+        /// <see cref="E:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter.Commit" />
+        /// event.
+        /// </summary>
+        protected virtual void OnCommit()
+        {
+            OnCommit(this, new RoutedEventArgs());
+        }
+
+        /// <summary>
+        /// Fires the Commit event.
+        /// </summary>
+        /// <param name="sender">The source object.</param>
+        /// <param name="e">The event data.</param>
+        private void OnCommit(object sender, RoutedEventArgs e)
+        {
+            Commit?.Invoke(sender, e);
+
+            AfterAdapterAction();
+        }
+
+        /// <summary>
+        /// Raises the
+        /// <see cref="E:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter.Cancel" />
+        /// event.
+        /// </summary>
+        protected virtual void OnCancel()
+        {
+            OnCancel(this, new RoutedEventArgs());
+        }
+
+        /// <summary>
+        /// Fires the Cancel event.
+        /// </summary>
+        /// <param name="sender">The source object.</param>
+        /// <param name="e">The event data.</param>
+        private void OnCancel(object sender, RoutedEventArgs e)
+        {
+            Cancel?.Invoke(sender, e);
+
+            AfterAdapterAction();
+        }
+
+        /// <summary>
+        /// Change the selection after the actions are complete.
+        /// </summary>
+        private void AfterAdapterAction()
+        {
+            IgnoringSelectionChanged = true;
+            if (SelectorControl != null)
+            {
+                SelectorControl.SelectedItem = null;
+                SelectorControl.SelectedIndex = -1;
+            }
+            IgnoringSelectionChanged = false;
+        }
+    }
+}

+ 5 - 0
src/Avalonia.Controls/Utils/UndoRedoHelper.cs

@@ -91,6 +91,11 @@ namespace Avalonia.Controls.Utils
             }
         }
 
+        public void Clear()
+        {
+            _states.Clear();
+        }
+
         bool WeakTimer.IWeakTimerSubscriber.Tick()
         {
             Snapshot();

+ 1 - 1
src/Avalonia.Input/KeyboardDevice.cs

@@ -46,13 +46,13 @@ namespace Avalonia.Input
             if (element != FocusedElement)
             {
                 var interactive = FocusedElement as IInteractive;
+                FocusedElement = element;
 
                 interactive?.RaiseEvent(new RoutedEventArgs
                 {
                     RoutedEvent = InputElement.LostFocusEvent,
                 });
 
-                FocusedElement = element;
                 interactive = element as IInteractive;
 
                 interactive?.RaiseEvent(new GotFocusEventArgs

+ 1 - 1
src/Avalonia.Themes.Default/Accents/BaseLight.xaml

@@ -20,7 +20,7 @@
     <SolidColorBrush x:Key="ErrorBrush">Red</SolidColorBrush>
     <SolidColorBrush x:Key="ErrorBrushLight">#10ff0000</SolidColorBrush>
 
-    <sys:Double x:Key="ThemeBorderThickness">2</sys:Double>
+    <Thickness x:Key="ThemeBorderThickness">2</Thickness>
     <sys:Double x:Key="ThemeDisabledOpacity">0.5</sys:Double>
 
     <sys:Double x:Key="FontSizeSmall">10</sys:Double>

+ 43 - 0
src/Avalonia.Themes.Default/AutoCompleteBox.xaml

@@ -0,0 +1,43 @@
+<Styles xmlns="https://github.com/avaloniaui">
+  <Style Selector="AutoCompleteBox">
+    <Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
+    <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
+    <Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
+    <Setter Property="Padding" Value="4"/>
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Panel>
+          <TextBox Name="PART_TextBox"
+                   Background="{TemplateBinding Background}" 
+                   BorderBrush="{TemplateBinding BorderBrush}" 
+                   BorderThickness="{TemplateBinding BorderThickness}"
+                   Padding="{TemplateBinding Padding}"
+                   Watermark="{TemplateBinding Watermark}"
+                   DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}" />
+          
+          <Popup Name="PART_Popup"
+                 MinWidth="{TemplateBinding Bounds.Width}"
+                 MaxHeight="{TemplateBinding MaxDropDownHeight}"
+                 PlacementTarget="{TemplateBinding}"
+                 StaysOpen="False">
+            <Border BorderBrush="{DynamicResource ThemeBorderMidBrush}"
+                    BorderThickness="1">
+              <ListBox Name="PART_SelectingItemsControl"
+                       BorderThickness="0"
+                       Background="{TemplateBinding Background}"
+                       Foreground="{TemplateBinding Foreground}"
+                       ItemTemplate="{TemplateBinding ItemTemplate}"
+                       MemberSelector="{TemplateBinding ValueMemberSelector}"
+                       ScrollViewer.HorizontalScrollBarVisibility="Auto"
+                       ScrollViewer.VerticalScrollBarVisibility="Auto" />
+            </Border>
+          </Popup>
+        </Panel>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+  
+  <Style Selector="AutoCompleteBox ListBoxItem:pointerover">
+    <Setter Property="Background" Value="#ffd0d0d0"/>
+  </Style>
+</Styles>

+ 3 - 1
src/Avalonia.Themes.Default/DefaultTheme.xaml

@@ -23,7 +23,7 @@
   <StyleInclude Source="resm:Avalonia.Themes.Default.RadioButton.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.RepeatButton.xaml?assembly=Avalonia.Themes.Default" />
   <StyleInclude Source="resm:Avalonia.Themes.Default.Separator.xaml?assembly=Avalonia.Themes.Default"/>
-  <StyleInclude Source="resm:Avalonia.Themes.Default.Slider.xaml?assembly=Avalonia.Themes.Default"/>  
+  <StyleInclude Source="resm:Avalonia.Themes.Default.Slider.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.ScrollBar.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.ScrollViewer.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.TabStrip.xaml?assembly=Avalonia.Themes.Default"/>
@@ -43,4 +43,6 @@
   <StyleInclude Source="resm:Avalonia.Themes.Default.Calendar.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.DatePicker.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.ButtonSpinner.xaml?assembly=Avalonia.Themes.Default"/>
+  <StyleInclude Source="resm:Avalonia.Themes.Default.NumericUpDown.xaml?assembly=Avalonia.Themes.Default"/>
+  <StyleInclude Source="resm:Avalonia.Themes.Default.AutoCompleteBox.xaml?assembly=Avalonia.Themes.Default"/>
 </Styles>

+ 4 - 0
src/Avalonia.Themes.Default/MenuItem.xaml

@@ -133,4 +133,8 @@
   <Style Selector="MenuItem:empty /template/ Path#rightArrow">
     <Setter Property="IsVisible" Value="False"/>
   </Style>
+
+  <Style Selector="MenuItem:disabled">
+    <Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}"/>
+  </Style>
 </Styles>

+ 41 - 0
src/Avalonia.Themes.Default/NumericUpDown.xaml

@@ -0,0 +1,41 @@
+<Styles xmlns="https://github.com/avaloniaui">
+  <Style Selector="NumericUpDown">
+    <Setter Property="TemplatedControl.BorderBrush" Value="{DynamicResource ThemeBorderLightBrush}"/>
+    <Setter Property="TemplatedControl.BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
+    <Setter Property="TemplatedControl.Background" Value="{DynamicResource ThemeBackgroundBrush}" />
+    <Setter Property="TemplatedControl.Foreground" Value="{DynamicResource ThemeForegroundBrush}" />
+    <Setter Property="TemplatedControl.Template">
+      <ControlTemplate>
+        <ButtonSpinner Name="PART_Spinner"
+                       Background="{TemplateBinding Background}"
+                       BorderThickness="{TemplateBinding BorderThickness}"
+                       BorderBrush="{TemplateBinding BorderBrush}"
+                       HorizontalContentAlignment="Stretch"
+                       VerticalContentAlignment="Stretch"
+                       AllowSpin="{TemplateBinding AllowSpin}"
+                       ShowButtonSpinner="{TemplateBinding ShowButtonSpinner}"
+                       ButtonSpinnerLocation="{TemplateBinding ButtonSpinnerLocation}">
+          <TextBox Name="PART_TextBox"
+                   BorderThickness="0"
+                   Background="Transparent"
+                   ContextMenu="{TemplateBinding ContextMenu}"
+                   FontFamily="{TemplateBinding FontFamily}"
+                   FontSize="{TemplateBinding FontSize}"
+                   FontStyle="{TemplateBinding FontStyle}"
+                   FontWeight="{TemplateBinding FontWeight}"
+                   Foreground="{TemplateBinding Foreground}"
+                   Watermark="{TemplateBinding Watermark}"
+                   IsReadOnly="{TemplateBinding IsReadOnly}"
+                   Text="{TemplateBinding Text}"
+                   Padding="{TemplateBinding Padding}"
+                   TextAlignment="Left"
+                   Margin="1"
+                   MinWidth="20"
+                   AcceptsReturn="False"
+                   TextWrapping="NoWrap">
+          </TextBox>
+        </ButtonSpinner>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+</Styles>

+ 97 - 0
src/Avalonia.Visuals/CornerRadius.cs

@@ -0,0 +1,97 @@
+// 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.Globalization;
+using System.Linq;
+
+namespace Avalonia
+{
+    public struct CornerRadius
+    {
+        public CornerRadius(double uniformRadius)
+        {
+            TopLeft = TopRight = BottomLeft = BottomRight = uniformRadius;
+
+        }
+        public CornerRadius(double top, double bottom)
+        {
+            TopLeft = TopRight = top;
+            BottomLeft = BottomRight = bottom;
+        }
+        public CornerRadius(double topLeft, double topRight, double bottomRight, double bottomLeft)
+        {
+            TopLeft = topLeft;
+            TopRight = topRight;
+            BottomRight = bottomRight;
+            BottomLeft = bottomLeft;
+        }
+
+        public double TopLeft { get; }
+        public double TopRight { get; }
+        public double BottomRight { get; }
+        public double BottomLeft { get; }
+        public bool IsEmpty => TopLeft.Equals(0) && IsUniform;
+        public bool IsUniform => TopLeft.Equals(TopRight) && BottomLeft.Equals(BottomRight) && TopRight.Equals(BottomRight);
+
+        public override bool Equals(object obj)
+        {
+            if (obj is CornerRadius)
+            {
+                return this == (CornerRadius)obj;
+            }
+            return false;
+        }
+
+        public override int GetHashCode()
+        {
+            return TopLeft.GetHashCode() ^ TopRight.GetHashCode() ^ BottomLeft.GetHashCode() ^ BottomRight.GetHashCode();
+        }
+
+        public override string ToString()
+        {
+            return $"{TopLeft},{TopRight},{BottomRight},{BottomLeft}";
+        }
+
+        public static CornerRadius Parse(string s, CultureInfo culture)
+        {
+            var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
+                .Select(x => x.Trim())
+                .ToList();
+
+            switch (parts.Count)
+            {
+                case 1:
+                    var uniform = double.Parse(parts[0], culture);
+                    return new CornerRadius(uniform);
+                case 2:
+                    var top = double.Parse(parts[0], culture);
+                    var bottom = double.Parse(parts[1], culture);
+                    return new CornerRadius(top, bottom);
+                case 4:
+                    var topLeft = double.Parse(parts[0], culture);
+                    var topRight = double.Parse(parts[1], culture);
+                    var bottomRight = double.Parse(parts[2], culture);
+                    var bottomLeft = double.Parse(parts[3], culture);
+                    return new CornerRadius(topLeft, topRight, bottomRight, bottomLeft);
+                default:
+                    {
+                        throw new FormatException("Invalid CornerRadius.");
+                    }
+            }
+        }
+
+        public static bool operator ==(CornerRadius cr1, CornerRadius cr2)
+        {
+            return cr1.TopLeft.Equals(cr2.TopLeft)
+                   && cr1.TopRight.Equals(cr2.TopRight)
+                   && cr1.BottomRight.Equals(cr2.BottomRight) 
+                   && cr1.BottomLeft.Equals(cr2.BottomLeft);
+        }
+
+        public static bool operator !=(CornerRadius cr1, CornerRadius cr2)
+        {
+            return !(cr1 == cr2);
+        }
+    }
+}

+ 1 - 1
src/Avalonia.Visuals/Media/GradientBrush.cs

@@ -22,7 +22,7 @@ namespace Avalonia.Media
         /// Defines the <see cref="GradientStops"/> property.
         /// </summary>
         public static readonly StyledProperty<IList<GradientStop>> GradientStopsProperty =
-            AvaloniaProperty.Register<GradientBrush, IList<GradientStop>>(nameof(Opacity));
+            AvaloniaProperty.Register<GradientBrush, IList<GradientStop>>(nameof(GradientStops));
 
         /// <summary>
         /// Initializes a new instance of the <see cref="GradientBrush"/> class.

+ 1 - 0
src/Avalonia.Visuals/Properties/AssemblyInfo.cs

@@ -11,6 +11,7 @@ using Avalonia.Metadata;
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Transitions")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Keyframes")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media")]
+[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")]
 
 [assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")]
 [assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests")]

+ 6 - 1
src/Avalonia.Visuals/Thickness.cs

@@ -90,7 +90,12 @@ namespace Avalonia
         /// <summary>
         /// Gets a value indicating whether all sides are set to 0.
         /// </summary>
-        public bool IsEmpty => Left == 0 && Top == 0 && Right == 0 && Bottom == 0;
+        public bool IsEmpty => Left.Equals(0) && IsUniform;
+
+        /// <summary>
+        /// Gets a value indicating whether all sides are equal.
+        /// </summary>
+        public bool IsUniform => Left.Equals(Right) && Top.Equals(Bottom) && Right.Equals(Bottom);
 
         /// <summary>
         /// Compares two Thicknesses.

+ 1 - 0
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@@ -32,6 +32,7 @@
         <Compile Include="AvaloniaXamlLoaderPortableXaml.cs" />
         <Compile Include="AvaloniaXamlLoader.cs" />
         <Compile Include="Converters\EasingTypeConverter.cs" />
+        <Compile Include="Converters\CornerRadiusTypeConverter.cs" />
         <Compile Include="Converters\MatrixTypeConverter.cs" />
         <Compile Include="Converters\RectTypeConverter.cs" />
         <Compile Include="Converters\SetterValueTypeConverter.cs" />

+ 19 - 0
src/Markup/Avalonia.Markup.Xaml/Converters/CornerRadiusTypeConverter.cs

@@ -0,0 +1,19 @@
+using System;
+using System.ComponentModel;
+using System.Globalization;
+
+namespace Avalonia.Markup.Xaml.Converters
+{
+    public class CornerRadiusTypeConverter : TypeConverter
+    {
+        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
+        {
+            return sourceType == typeof(string);
+        }
+
+        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+        {
+            return CornerRadius.Parse((string)value, culture);
+        }
+    }
+}

+ 1 - 0
src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs

@@ -45,6 +45,7 @@ namespace Avalonia.Markup.Xaml.PortableXaml
             { typeof(Selector), typeof(SelectorTypeConverter)},
             { typeof(SolidColorBrush), typeof(BrushTypeConverter) },
             { typeof(Thickness), typeof(ThicknessTypeConverter) },
+            { typeof(CornerRadius), typeof(CornerRadiusTypeConverter) },
             { typeof(TimeSpan), typeof(TimeSpanTypeConverter) },
             //{ typeof(Uri), typeof(Converters.UriTypeConverter) },
             { typeof(Cursor), typeof(CursorTypeConverter) },

+ 2 - 5
src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs

@@ -1,7 +1,6 @@
 // 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.Platform;
 using SharpDX.Direct2D1;
 
@@ -20,14 +19,12 @@ namespace Avalonia.Direct2D1.Media
         /// <inheritdoc/>
         public Rect Bounds => Geometry.GetWidenedBounds(0).ToAvalonia();
 
-        /// <inheritdoc/>
         public Geometry Geometry { get; }
 
         /// <inheritdoc/>
         public Rect GetRenderBounds(Avalonia.Media.Pen pen)
         {
-            var factory = AvaloniaLocator.Current.GetService<Factory>();
-            return Geometry.GetWidenedBounds((float)pen.Thickness).ToAvalonia();
+            return Geometry.GetWidenedBounds((float)(pen?.Thickness ?? 0)).ToAvalonia();
         }
 
         /// <inheritdoc/>
@@ -51,7 +48,7 @@ namespace Avalonia.Direct2D1.Media
         /// <inheritdoc/>
         public bool StrokeContains(Avalonia.Media.Pen pen, Point point)
         {
-            return Geometry.StrokeContainsPoint(point.ToSharpDX(), (float)pen.Thickness);
+            return Geometry.StrokeContainsPoint(point.ToSharpDX(), (float)(pen?.Thickness ?? 0));
         }
 
         public ITransformedGeometryImpl WithTransform(Matrix transform)

+ 1 - 1
tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs

@@ -33,7 +33,7 @@ namespace Avalonia.Benchmarks.Styling
 
             var border = (Border)textBox.GetVisualChildren().Single();
 
-            if (border.BorderThickness != 2)
+            if (border.BorderThickness != new Thickness(2))
             {
                 throw new Exception("Styles not applied.");
             }

+ 1042 - 0
tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs

@@ -0,0 +1,1042 @@
+// 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.ComponentModel;
+using System.Linq;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Templates;
+using Avalonia.Data;
+using Avalonia.Markup.Xaml.Data;
+using Avalonia.Platform;
+using Avalonia.Threading;
+using Avalonia.UnitTests;
+using Moq;
+using Xunit;
+using System.Collections.ObjectModel;
+
+namespace Avalonia.Controls.UnitTests
+{
+    public class AutoCompleteBoxTests
+    {
+        [Fact]
+        public void Search_Filters()
+        {
+            Assert.True(GetFilter(AutoCompleteFilterMode.Contains)("am", "name"));
+            Assert.True(GetFilter(AutoCompleteFilterMode.Contains)("AME", "name"));
+            Assert.False(GetFilter(AutoCompleteFilterMode.Contains)("hello", "name"));
+
+            Assert.True(GetFilter(AutoCompleteFilterMode.ContainsCaseSensitive)("na", "name"));
+            Assert.False(GetFilter(AutoCompleteFilterMode.ContainsCaseSensitive)("AME", "name"));
+            Assert.False(GetFilter(AutoCompleteFilterMode.ContainsCaseSensitive)("hello", "name"));
+
+            Assert.Null(GetFilter(AutoCompleteFilterMode.Custom));
+            Assert.Null(GetFilter(AutoCompleteFilterMode.None));
+
+            Assert.True(GetFilter(AutoCompleteFilterMode.Equals)("na", "na"));
+            Assert.True(GetFilter(AutoCompleteFilterMode.Equals)("na", "NA"));
+            Assert.False(GetFilter(AutoCompleteFilterMode.Equals)("hello", "name"));
+
+            Assert.True(GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive)("na", "na"));
+            Assert.False(GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive)("na", "NA"));
+            Assert.False(GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive)("hello", "name"));
+
+            Assert.True(GetFilter(AutoCompleteFilterMode.StartsWith)("na", "name"));
+            Assert.True(GetFilter(AutoCompleteFilterMode.StartsWith)("NAM", "name"));
+            Assert.False(GetFilter(AutoCompleteFilterMode.StartsWith)("hello", "name"));
+
+            Assert.True(GetFilter(AutoCompleteFilterMode.StartsWithCaseSensitive)("na", "name"));
+            Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithCaseSensitive)("NAM", "name"));
+            Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithCaseSensitive)("hello", "name"));
+        }
+
+        [Fact]
+        public void Ordinal_Search_Filters()
+        {
+            Assert.True(GetFilter(AutoCompleteFilterMode.ContainsOrdinal)("am", "name"));
+            Assert.True(GetFilter(AutoCompleteFilterMode.ContainsOrdinal)("AME", "name"));
+            Assert.False(GetFilter(AutoCompleteFilterMode.ContainsOrdinal)("hello", "name"));
+
+            Assert.True(GetFilter(AutoCompleteFilterMode.ContainsOrdinalCaseSensitive)("na", "name"));
+            Assert.False(GetFilter(AutoCompleteFilterMode.ContainsOrdinalCaseSensitive)("AME", "name"));
+            Assert.False(GetFilter(AutoCompleteFilterMode.ContainsOrdinalCaseSensitive)("hello", "name"));
+
+            Assert.True(GetFilter(AutoCompleteFilterMode.EqualsOrdinal)("na", "na"));
+            Assert.True(GetFilter(AutoCompleteFilterMode.EqualsOrdinal)("na", "NA"));
+            Assert.False(GetFilter(AutoCompleteFilterMode.EqualsOrdinal)("hello", "name"));
+
+            Assert.True(GetFilter(AutoCompleteFilterMode.EqualsOrdinalCaseSensitive)("na", "na"));
+            Assert.False(GetFilter(AutoCompleteFilterMode.EqualsOrdinalCaseSensitive)("na", "NA"));
+            Assert.False(GetFilter(AutoCompleteFilterMode.EqualsOrdinalCaseSensitive)("hello", "name"));
+
+            Assert.True(GetFilter(AutoCompleteFilterMode.StartsWithOrdinal)("na", "name"));
+            Assert.True(GetFilter(AutoCompleteFilterMode.StartsWithOrdinal)("NAM", "name"));
+            Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithOrdinal)("hello", "name"));
+
+            Assert.True(GetFilter(AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive)("na", "name"));
+            Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive)("NAM", "name"));
+            Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive)("hello", "name"));
+        }
+
+        [Fact]
+        public void Fires_DropDown_Events()
+        {
+            RunTest((control, textbox) =>
+            {
+                bool openEvent = false;
+                bool closeEvent = false;
+                control.DropDownOpened += (s, e) => openEvent = true;
+                control.DropDownClosed += (s, e) => closeEvent = true;
+                control.Items = CreateSimpleStringArray();
+
+                textbox.Text = "a";
+                Dispatcher.UIThread.RunJobs();
+                Assert.True(control.SearchText == "a");
+                Assert.True(control.IsDropDownOpen);
+                Assert.True(openEvent);
+
+                textbox.Text = String.Empty;
+                Dispatcher.UIThread.RunJobs();
+                Assert.True(control.SearchText == String.Empty);
+                Assert.False(control.IsDropDownOpen);
+                Assert.True(closeEvent);
+            });
+        }
+
+        [Fact]
+        public void Text_Completion_Via_Text_Property()
+        {
+            RunTest((control, textbox) =>
+            {
+                control.IsTextCompletionEnabled = true;
+
+                Assert.Equal(String.Empty, control.Text);
+                control.Text = "close";
+                Assert.NotNull(control.SelectedItem);
+            });
+        }
+
+        [Fact]
+        public void Text_Completion_Selects_Text()
+        {
+            RunTest((control, textbox) =>
+            {
+                control.IsTextCompletionEnabled = true;
+
+                textbox.Text = "ac";
+                textbox.SelectionEnd = textbox.SelectionStart = 2;
+                Dispatcher.UIThread.RunJobs();
+
+                Assert.True(control.IsDropDownOpen);
+                Assert.True(Math.Abs(textbox.SelectionEnd - textbox.SelectionStart) > 2);
+            });
+        }
+
+        [Fact]
+        public void TextChanged_Event_Fires()
+        {
+            RunTest((control, textbox) =>
+            {
+                bool textChanged = false;
+                control.TextChanged += (s, e) => textChanged = true;
+
+                textbox.Text = "a";
+                Dispatcher.UIThread.RunJobs();
+                Assert.True(textChanged);
+
+                textChanged = false;
+                control.Text = "conversati";
+                Dispatcher.UIThread.RunJobs();
+                Assert.True(textChanged);
+
+                textChanged = false;
+                control.Text = null;
+                Dispatcher.UIThread.RunJobs();
+                Assert.True(textChanged);
+            });
+        }
+
+        [Fact]
+        public void MinimumPrefixLength_Works()
+        {
+            RunTest((control, textbox) =>
+            {
+                textbox.Text = "a";
+                Dispatcher.UIThread.RunJobs();
+                Assert.True(control.IsDropDownOpen);
+
+
+                textbox.Text = String.Empty;
+                Dispatcher.UIThread.RunJobs();
+                Assert.False(control.IsDropDownOpen);
+
+                control.MinimumPrefixLength = 3;
+
+                textbox.Text = "a";
+                Dispatcher.UIThread.RunJobs();
+                Assert.False(control.IsDropDownOpen);
+
+                textbox.Text = "acc";
+                Dispatcher.UIThread.RunJobs();
+                Assert.True(control.IsDropDownOpen);
+            });
+        }
+
+        [Fact]
+        public void Can_Cancel_DropDown_Opening()
+        {
+            RunTest((control, textbox) =>
+            {
+                control.DropDownOpening += (s, e) => e.Cancel = true;
+
+                textbox.Text = "a";
+                Dispatcher.UIThread.RunJobs();
+                Assert.False(control.IsDropDownOpen);
+            });
+        }
+
+        [Fact]
+        public void Can_Cancel_DropDown_Closing()
+        {
+            RunTest((control, textbox) =>
+            {
+                control.DropDownClosing += (s, e) => e.Cancel = true;
+
+                textbox.Text = "a";
+                Dispatcher.UIThread.RunJobs();
+                Assert.True(control.IsDropDownOpen);
+
+                control.IsDropDownOpen = false;
+                Assert.True(control.IsDropDownOpen);
+            });
+        }
+
+        [Fact]
+        public void Can_Cancel_Population()
+        {
+            RunTest((control, textbox) =>
+            {
+                bool populating = false;
+                bool populated = false;
+                control.FilterMode = AutoCompleteFilterMode.None;
+                control.Populating += (s, e) =>
+                {
+                    e.Cancel = true;
+                    populating = true;
+                };
+                control.Populated += (s, e) => populated = true;
+
+                textbox.Text = "accounti";
+                Dispatcher.UIThread.RunJobs();
+
+                Assert.True(populating);
+                Assert.False(populated);
+            });
+        }
+
+        [Fact]
+        public void Custom_Population_Supported()
+        {
+            RunTest((control, textbox) =>
+            {
+                string custom = "Custom!";
+                string search = "accounti";
+                bool populated = false;
+                bool populatedOk = false;
+                control.FilterMode = AutoCompleteFilterMode.None;
+                control.Populating += (s, e) =>
+                {
+                    control.Items = new string[] { custom };
+                    Assert.Equal(search, e.Parameter);
+                };
+                control.Populated += (s, e) =>
+                {
+                    populated = true;
+                    ReadOnlyCollection<object> collection = e.Data as ReadOnlyCollection<object>;
+                    populatedOk = collection != null && collection.Count == 1;
+                };
+
+                textbox.Text = search;
+                Dispatcher.UIThread.RunJobs();
+
+                Assert.True(populated);
+                Assert.True(populatedOk);
+            });
+        }
+
+        [Fact]
+        public void Text_Completion()
+        {
+            RunTest((control, textbox) =>
+            {
+                control.IsTextCompletionEnabled = true;
+                textbox.Text = "accounti";
+                textbox.SelectionStart = textbox.SelectionEnd = textbox.Text.Length;
+                Dispatcher.UIThread.RunJobs();
+                Assert.Equal("accounti", control.SearchText);
+                Assert.Equal("accounting", textbox.Text);
+            });
+        }
+
+        [Fact]
+        public void String_Search()
+        {
+            RunTest((control, textbox) =>
+            {
+                textbox.Text = "a";
+                Dispatcher.UIThread.RunJobs();
+                Assert.Equal(textbox.Text, control.Text);
+
+                textbox.Text = "acc";
+                Dispatcher.UIThread.RunJobs();
+                Assert.Equal(textbox.Text, control.Text);
+
+                textbox.Text = "a";
+                Dispatcher.UIThread.RunJobs();
+                Assert.Equal(textbox.Text, control.Text);
+
+                textbox.Text = "";
+                Dispatcher.UIThread.RunJobs();
+                Assert.Equal(textbox.Text, control.Text);
+
+                textbox.Text = "cook";
+                Dispatcher.UIThread.RunJobs();
+                Assert.Equal(textbox.Text, control.Text);
+
+                textbox.Text = "accept";
+                Dispatcher.UIThread.RunJobs();
+                Assert.Equal(textbox.Text, control.Text);
+
+                textbox.Text = "cook";
+                Dispatcher.UIThread.RunJobs();
+                Assert.Equal(textbox.Text, control.Text);
+            });
+        }
+
+        [Fact]
+        public void Item_Search()
+        {
+            RunTest((control, textbox) =>
+            {
+                control.FilterMode = AutoCompleteFilterMode.Custom;
+                control.ItemFilter = (search, item) =>
+                {
+                    string s = item as string;
+                    return s == null ? false : true;
+                };
+
+                // Just set to null briefly to exercise that code path
+                AutoCompleteFilterPredicate<object> filter = control.ItemFilter;
+                Assert.NotNull(filter);
+                control.ItemFilter = null;
+                Assert.Null(control.ItemFilter);
+                control.ItemFilter = filter;
+                Assert.NotNull(control.ItemFilter);
+
+                textbox.Text = "a";
+                Dispatcher.UIThread.RunJobs();
+                Assert.Equal(textbox.Text, control.Text);
+
+                textbox.Text = "acc";
+                Dispatcher.UIThread.RunJobs();
+                Assert.Equal(textbox.Text, control.Text);
+
+                textbox.Text = "a";
+                Dispatcher.UIThread.RunJobs();
+                Assert.Equal(textbox.Text, control.Text);
+
+                textbox.Text = "";
+                Dispatcher.UIThread.RunJobs();
+                Assert.Equal(textbox.Text, control.Text);
+
+                textbox.Text = "cook";
+                Dispatcher.UIThread.RunJobs();
+                Assert.Equal(textbox.Text, control.Text);
+
+                textbox.Text = "accept";
+                Dispatcher.UIThread.RunJobs();
+                Assert.Equal(textbox.Text, control.Text);
+
+                textbox.Text = "cook";
+                Dispatcher.UIThread.RunJobs();
+                Assert.Equal(textbox.Text, control.Text);
+            });
+        }
+        
+        /// <summary>
+        /// Retrieves a defined predicate filter through a new AutoCompleteBox 
+        /// control instance.
+        /// </summary>
+        /// <param name="mode">The FilterMode of interest.</param>
+        /// <returns>Returns the predicate instance.</returns>
+        private static AutoCompleteFilterPredicate<string> GetFilter(AutoCompleteFilterMode mode)
+        {
+            return new AutoCompleteBox { FilterMode = mode }
+                .TextFilter;
+        }
+
+        /// <summary>
+        /// Creates a large list of strings for AutoCompleteBox testing.
+        /// </summary>
+        /// <returns>Returns a new List of string values.</returns>
+        private IList<string> CreateSimpleStringArray()
+        {
+            return new List<string>
+            {
+            "a",
+            "abide",
+            "able",
+            "about",
+            "above",
+            "absence",
+            "absurd",
+            "accept",
+            "acceptance",
+            "accepted",
+            "accepting",
+            "access",
+            "accessed",
+            "accessible",
+            "accident",
+            "accidentally",
+            "accordance",
+            "account",
+            "accounting",
+            "accounts",
+            "accusation",
+            "accustomed",
+            "ache",
+            "across",
+            "act",
+            "active",
+            "actual",
+            "actually",
+            "ada",
+            "added",
+            "adding",
+            "addition",
+            "additional",
+            "additions",
+            "address",
+            "addressed",
+            "addresses",
+            "addressing",
+            "adjourn",
+            "adoption",
+            "advance",
+            "advantage",
+            "adventures",
+            "advice",
+            "advisable",
+            "advise",
+            "affair",
+            "affectionately",
+            "afford",
+            "afore",
+            "afraid",
+            "after",
+            "afterwards",
+            "again",
+            "against",
+            "age",
+            "aged",
+            "agent",
+            "ago",
+            "agony",
+            "agree",
+            "agreed",
+            "agreement",
+            "ah",
+            "ahem",
+            "air",
+            "airs",
+            "ak",
+            "alarm",
+            "alarmed",
+            "alas",
+            "alice",
+            "alive",
+            "all",
+            "allow",
+            "almost",
+            "alone",
+            "along",
+            "aloud",
+            "already",
+            "also",
+            "alteration",
+            "altered",
+            "alternate",
+            "alternately",
+            "altogether",
+            "always",
+            "am",
+            "ambition",
+            "among",
+            "an",
+            "ancient",
+            "and",
+            "anger",
+            "angrily",
+            "angry",
+            "animal",
+            "animals",
+            "ann",
+            "annoy",
+            "annoyed",
+            "another",
+            "answer",
+            "answered",
+            "answers",
+            "antipathies",
+            "anxious",
+            "anxiously",
+            "any",
+            "anyone",
+            "anything",
+            "anywhere",
+            "appealed",
+            "appear",
+            "appearance",
+            "appeared",
+            "appearing",
+            "appears",
+            "applause",
+            "apple",
+            "apples",
+            "applicable",
+            "apply",
+            "approach",
+            "arch",
+            "archbishop",
+            "arches",
+            "archive",
+            "are",
+            "argue",
+            "argued",
+            "argument",
+            "arguments",
+            "arise",
+            "arithmetic",
+            "arm",
+            "arms",
+            "around",
+            "arranged",
+            "array",
+            "arrived",
+            "arrow",
+            "arrum",
+            "as",
+            "ascii",
+            "ashamed",
+            "ask",
+            "askance",
+            "asked",
+            "asking",
+            "asleep",
+            "assembled",
+            "assistance",
+            "associated",
+            "at",
+            "ate",
+            "atheling",
+            "atom",
+            "attached",
+            "attempt",
+            "attempted",
+            "attempts",
+            "attended",
+            "attending",
+            "attends",
+            "audibly",
+            "australia",
+            "author",
+            "authority",
+            "available",
+            "avoid",
+            "away",
+            "awfully",
+            "axes",
+            "axis",
+            "b",
+            "baby",
+            "back",
+            "backs",
+            "bad",
+            "bag",
+            "baked",
+            "balanced",
+            "bank",
+            "banks",
+            "banquet",
+            "bark",
+            "barking",
+            "barley",
+            "barrowful",
+            "based",
+            "bat",
+            "bathing",
+            "bats",
+            "bawled",
+            "be",
+            "beak",
+            "bear",
+            "beast",
+            "beasts",
+            "beat",
+            "beating",
+            "beau",
+            "beauti",
+            "beautiful",
+            "beautifully",
+            "beautify",
+            "became",
+            "because",
+            "become",
+            "becoming",
+            "bed",
+            "beds",
+            "bee",
+            "been",
+            "before",
+            "beg",
+            "began",
+            "begged",
+            "begin",
+            "beginning",
+            "begins",
+            "begun",
+            "behead",
+            "beheaded",
+            "beheading",
+            "behind",
+            "being",
+            "believe",
+            "believed",
+            "bells",
+            "belong",
+            "belongs",
+            "beloved",
+            "below",
+            "belt",
+            "bend",
+            "bent",
+            "besides",
+            "best",
+            "better",
+            "between",
+            "bill",
+            "binary",
+            "bird",
+            "birds",
+            "birthday",
+            "bit",
+            "bite",
+            "bitter",
+            "blacking",
+            "blades",
+            "blame",
+            "blasts",
+            "bleeds",
+            "blew",
+            "blow",
+            "blown",
+            "blows",
+            "body",
+            "boldly",
+            "bone",
+            "bones",
+            "book",
+            "books",
+            "boon",
+            "boots",
+            "bore",
+            "both",
+            "bother",
+            "bottle",
+            "bottom",
+            "bough",
+            "bound",
+            "bowed",
+            "bowing",
+            "box",
+            "boxed",
+            "boy",
+            "brain",
+            "branch",
+            "branches",
+            "brandy",
+            "brass",
+            "brave",
+            "breach",
+            "bread",
+            "break",
+            "breath",
+            "breathe",
+            "breeze",
+            "bright",
+            "brightened",
+            "bring",
+            "bringing",
+            "bristling",
+            "broke",
+            "broken",
+            "brother",
+            "brought",
+            "brown",
+            "brush",
+            "brushing",
+            "burn",
+            "burning",
+            "burnt",
+            "burst",
+            "bursting",
+            "busily",
+            "business",
+            "business@pglaf",
+            "busy",
+            "but",
+            "butter",
+            "buttercup",
+            "buttered",
+            "butterfly",
+            "buttons",
+            "by",
+            "bye",
+            "c",
+            "cackled",
+            "cake",
+            "cakes",
+            "calculate",
+            "calculated",
+            "call",
+            "called",
+            "calling",
+            "calmly",
+            "came",
+            "camomile",
+            "can",
+            "canary",
+            "candle",
+            "cannot",
+            "canterbury",
+            "canvas",
+            "capering",
+            "capital",
+            "card",
+            "cardboard",
+            "cards",
+            "care",
+            "carefully",
+            "cares",
+            "carried",
+            "carrier",
+            "carroll",
+            "carry",
+            "carrying",
+            "cart",
+            "cartwheels",
+            "case",
+            "cat",
+            "catch",
+            "catching",
+            "caterpillar",
+            "cats",
+            "cattle",
+            "caucus",
+            "caught",
+            "cauldron",
+            "cause",
+            "caused",
+            "cautiously",
+            "cease",
+            "ceiling",
+            "centre",
+            "certain",
+            "certainly",
+            "chain",
+            "chains",
+            "chair",
+            "chance",
+            "chanced",
+            "change",
+            "changed",
+            "changes",
+            "changing",
+            "chapter",
+            "character",
+            "charge",
+            "charges",
+            "charitable",
+            "charities",
+            "chatte",
+            "cheap",
+            "cheated",
+            "check",
+            "checked",
+            "checks",
+            "cheeks",
+            "cheered",
+            "cheerfully",
+            "cherry",
+            "cheshire",
+            "chief",
+            "child",
+            "childhood",
+            "children",
+            "chimney",
+            "chimneys",
+            "chin",
+            "choice",
+            "choke",
+            "choked",
+            "choking",
+            "choose",
+            "choosing",
+            "chop",
+            "chorus",
+            "chose",
+            "christmas",
+            "chrysalis",
+            "chuckled",
+            "circle",
+            "circumstances",
+            "city",
+            "civil",
+            "claim",
+            "clamour",
+            "clapping",
+            "clasped",
+            "classics",
+            "claws",
+            "clean",
+            "clear",
+            "cleared",
+            "clearer",
+            "clearly",
+            "clever",
+            "climb",
+            "clinging",
+            "clock",
+            "close",
+            "closed",
+            "closely",
+            "closer",
+            "clubs",
+            "coast",
+            "coaxing",
+            "codes",
+            "coils",
+            "cold",
+            "collar",
+            "collected",
+            "collection",
+            "come",
+            "comes",
+            "comfits",
+            "comfort",
+            "comfortable",
+            "comfortably",
+            "coming",
+            "commercial",
+            "committed",
+            "common",
+            "commotion",
+            "company",
+            "compilation",
+            "complained",
+            "complaining",
+            "completely",
+            "compliance",
+            "comply",
+            "complying",
+            "compressed",
+            "computer",
+            "computers",
+            "concept",
+            "concerning",
+            "concert",
+            "concluded",
+            "conclusion",
+            "condemn",
+            "conduct",
+            "confirmation",
+            "confirmed",
+            "confused",
+            "confusing",
+            "confusion",
+            "conger",
+            "conqueror",
+            "conquest",
+            "consented",
+            "consequential",
+            "consider",
+            "considerable",
+            "considered",
+            "considering",
+            "constant",
+            "consultation",
+            "contact",
+            "contain",
+            "containing",
+            "contempt",
+            "contemptuous",
+            "contemptuously",
+            "content",
+            "continued",
+            "contract",
+            "contradicted",
+            "contributions",
+            "conversation",
+            "conversations",
+            "convert",
+            "cook",
+            "cool",
+            "copied",
+            "copies",
+            "copy",
+            "copying",
+            "copyright",
+            "corner",
+            "corners",
+            "corporation",
+            "corrupt",
+            "cost",
+            "costs",
+            "could",
+            "couldn",
+            "counting",
+            "countries",
+            "country",
+            "couple",
+            "couples",
+            "courage",
+            "course",
+            "court",
+            "courtiers",
+            "coward",
+            "crab",
+            "crash",
+            "crashed",
+            "crawled",
+            "crawling",
+            "crazy",
+            "created",
+            "creating",
+            "creation",
+            "creature",
+            "creatures",
+            "credit",
+            "creep",
+            "crept",
+            "cried",
+            "cries",
+            "crimson",
+            "critical",
+            "crocodile",
+            "croquet",
+            "croqueted",
+            "croqueting",
+            "cross",
+            "crossed",
+            "crossly",
+            "crouched",
+            "crowd",
+            "crowded",
+            "crown",
+            "crumbs",
+            "crust",
+            "cry",
+            "crying",
+            "cucumber",
+            "cunning",
+            "cup",
+            "cupboards",
+            "cur",
+            "curiosity",
+            "curious",
+            "curiouser",
+            "curled",
+            "curls",
+            "curly",
+            "currants",
+            "current",
+            "curtain",
+            "curtsey",
+            "curtseying",
+            "curving",
+            "cushion",
+            "custard",
+            "custody",
+            "cut",
+            "cutting",
+            };
+        }
+        private void RunTest(Action<AutoCompleteBox, TextBox> test)
+        {
+            using (UnitTestApplication.Start(Services))
+            {
+                AutoCompleteBox control = CreateControl();
+                control.Items = CreateSimpleStringArray();
+                TextBox textBox = GetTextBox(control);
+                Dispatcher.UIThread.RunJobs();
+                test.Invoke(control, textBox);
+            }
+        }
+
+        private static TestServices Services => TestServices.StyledWindow;
+
+        /*private static TestServices Services => TestServices.MockThreadingInterface.With(
+            standardCursorFactory: Mock.Of<IStandardCursorFactory>(),
+            windowingPlatform: new MockWindowingPlatform());*/
+
+        private AutoCompleteBox CreateControl()
+        {
+            var datePicker =
+                new AutoCompleteBox
+                {
+                    Template = CreateTemplate()
+                };
+
+            datePicker.ApplyTemplate();
+            return datePicker;
+        }
+        private TextBox GetTextBox(AutoCompleteBox control)
+        {
+            return control.GetTemplateChildren()
+                          .OfType<TextBox>()
+                          .First();
+        }
+        private IControlTemplate CreateTemplate()
+        {
+            return new FuncControlTemplate<AutoCompleteBox>(control =>
+            {
+                var textBox =
+                    new TextBox
+                    {
+                        Name = "PART_TextBox"
+                    };
+                var listbox =
+                    new ListBox
+                    {
+                        Name = "PART_SelectingItemsControl"
+                    };
+                var popup =
+                    new Popup
+                    {
+                        Name = "PART_Popup"
+                    };
+
+                var panel = new Panel();
+                panel.Children.Add(textBox);
+                panel.Children.Add(popup);
+                panel.Children.Add(listbox);
+
+                return panel;
+            });
+        }
+    }
+}

+ 1 - 1
tests/Avalonia.Controls.UnitTests/BorderTests.cs

@@ -13,7 +13,7 @@ namespace Avalonia.Controls.UnitTests
             var target = new Border
             {
                 Padding = new Thickness(6),
-                BorderThickness = 4,
+                BorderThickness = new Thickness(4)
             };
 
             target.Measure(new Size(100, 100));

+ 25 - 0
tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs

@@ -80,6 +80,31 @@ namespace Avalonia.Controls.UnitTests.Presenters
             Assert.Equal(new Rect(expectedX, expectedY, expectedWidth, expectedHeight), content.Bounds);
         }
 
+        [Fact]
+        public void Should_Correctly_Align_Child_With_Fixed_Size()
+        {
+            Border content;
+            var target = new ContentPresenter
+            {
+                HorizontalContentAlignment = HorizontalAlignment.Stretch,
+                VerticalContentAlignment = VerticalAlignment.Stretch,
+                Content = content = new Border
+                {
+                    HorizontalAlignment = HorizontalAlignment.Left,
+                    VerticalAlignment = VerticalAlignment.Bottom,
+                    Width = 16,
+                    Height = 16,
+                },
+            };
+
+            target.UpdateChild();
+            target.Measure(new Size(100, 100));
+            target.Arrange(new Rect(0, 0, 100, 100));
+
+            // Check correct result for Issue #1447.
+            Assert.Equal(new Rect(0, 84, 16, 16), content.Bounds);
+        }
+
         [Fact]
         public void Content_Can_Be_Stretched()
         {

+ 53 - 13
tests/Avalonia.RenderTests/Controls/BorderTests.cs

@@ -31,7 +31,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 1,
+                    BorderThickness = new Thickness(1),
                 }
             };
 
@@ -50,7 +50,47 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
+                }
+            };
+
+            await RenderToFile(target);
+            CompareImages();
+        }
+
+        [Fact]
+        public async Task Border_Uniform_CornerRadius()
+        {
+            Decorator target = new Decorator
+            {
+                Padding = new Thickness(8),
+                Width = 200,
+                Height = 200,
+                Child = new Border
+                {
+                    BorderBrush = Brushes.Black,
+                    BorderThickness = new Thickness(2),
+                    CornerRadius = new CornerRadius(16),
+                }
+            };
+
+            await RenderToFile(target);
+            CompareImages();
+        }
+
+        [Fact]
+        public async Task Border_NonUniform_CornerRadius()
+        {
+            Decorator target = new Decorator
+            {
+                Padding = new Thickness(8),
+                Width = 200,
+                Height = 200,
+                Child = new Border
+                {
+                    BorderBrush = Brushes.Black,
+                    BorderThickness = new Thickness(2),
+                    CornerRadius = new CornerRadius(16, 4, 7, 10),
                 }
             };
 
@@ -87,7 +127,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
                     Child = new Border
                     {
                         Background = Brushes.Red,
@@ -110,7 +150,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
                     Padding = new Thickness(2),
                     Child = new Border
                     {
@@ -134,7 +174,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
                     Child = new Border
                     {
                         Background = Brushes.Red,
@@ -159,7 +199,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
                     Child = new TextBlock
                     {
                         Text = "Foo",
@@ -186,7 +226,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
                     Child = new TextBlock
                     {
                         Text = "Foo",
@@ -213,7 +253,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
                     Child = new TextBlock
                     {
                         Text = "Foo",
@@ -240,7 +280,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
                     Child = new TextBlock
                     {
                         Text = "Foo",
@@ -267,7 +307,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
                     Child = new TextBlock
                     {
                         Text = "Foo",
@@ -294,7 +334,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
                     Child = new TextBlock
                     {
                         Text = "Foo",
@@ -321,7 +361,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
                     Child = new TextBlock
                     {
                         Text = "Foo",
@@ -348,7 +388,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
                     Child = new TextBlock
                     {
                         Text = "Foo",

+ 1 - 1
tests/Avalonia.RenderTests/Media/VisualBrushTests.cs

@@ -42,7 +42,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
                         new Border
                         {
                             BorderBrush = Brushes.Blue,
-                            BorderThickness = 2,
+                            BorderThickness = new Thickness(2),
                             HorizontalAlignment = HorizontalAlignment.Center,
                             VerticalAlignment = VerticalAlignment.Center,
                             Child = new TextBlock

+ 1 - 1
tests/Avalonia.RenderTests/Shapes/PathTests.cs

@@ -316,7 +316,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes
                 Child = new Border
                 {
                     BorderBrush = Brushes.Red,
-                    BorderThickness = 1,
+                    BorderThickness = new Thickness(1),
                     HorizontalAlignment = HorizontalAlignment.Center,
                     VerticalAlignment = VerticalAlignment.Center,
                     Child = new Path

+ 3 - 3
tests/Avalonia.Styling.UnitTests/StyleTests.cs

@@ -151,7 +151,7 @@ namespace Avalonia.Styling.UnitTests
             {
                 Setters = new[]
                 {
-                    new Setter(Border.BorderThicknessProperty, 4),
+                    new Setter(Border.BorderThicknessProperty, new Thickness(4)),
                 }
             };
 
@@ -162,9 +162,9 @@ namespace Avalonia.Styling.UnitTests
 
             style.Attach(border, null);
 
-            Assert.Equal(4, border.BorderThickness);
+            Assert.Equal(new Thickness(4), border.BorderThickness);
             root.Child = null;
-            Assert.Equal(0, border.BorderThickness);
+            Assert.Equal(new Thickness(0), border.BorderThickness);
         }
 
         private class Class1 : Control

+ 43 - 0
tests/Avalonia.Visuals.UnitTests/CornerRadiusTests.cs

@@ -0,0 +1,43 @@
+// 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.Globalization;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests
+{
+    public class CornerRadiusTests
+    {
+        [Fact]
+        public void Parse_Parses_Single_Uniform_Radius()
+        {
+            var result = CornerRadius.Parse("3.4", CultureInfo.InvariantCulture);
+
+            Assert.Equal(new CornerRadius(3.4), result);
+        }
+
+        [Fact]
+        public void Parse_Parses_Top_Bottom()
+        {
+            var result = CornerRadius.Parse("1.1,2.2", CultureInfo.InvariantCulture);
+
+            Assert.Equal(new CornerRadius(1.1, 2.2), result);
+        }
+
+        [Fact]
+        public void Parse_Parses_TopLeft_TopRight_BottomRight_BottomLeft()
+        {
+            var result = CornerRadius.Parse("1.1,2.2,3.3,4.4", CultureInfo.InvariantCulture);
+
+            Assert.Equal(new CornerRadius(1.1, 2.2, 3.3, 4.4), result);
+        }
+
+        [Fact]
+        public void Parse_Accepts_Spaces()
+        {
+            var result = CornerRadius.Parse("1.1 2.2 3.3 4.4", CultureInfo.InvariantCulture);
+
+            Assert.Equal(new CornerRadius(1.1, 2.2, 3.3, 4.4), result);
+        }
+    }
+}

+ 2 - 2
tests/Avalonia.Visuals.UnitTests/ThicknessTests.cs

@@ -4,7 +4,7 @@
 using System.Globalization;
 using Xunit;
 
-namespace Avalonia.Visuals.UnitTests.Media
+namespace Avalonia.Visuals.UnitTests
 {
     public class ThicknessTests
     {
@@ -40,4 +40,4 @@ namespace Avalonia.Visuals.UnitTests.Media
             Assert.Equal(new Thickness(1.2, 3.4, 5, 6), result);
         }
     }
-}
+}

BIN
tests/TestFiles/Direct2D1/Controls/Border/Border_NonUniform_CornerRadius.expected.png


BIN
tests/TestFiles/Direct2D1/Controls/Border/Border_Uniform_CornerRadius.expected.png


BIN
tests/TestFiles/Skia/Controls/Border/Border_NonUniform_CornerRadius.expected.png


BIN
tests/TestFiles/Skia/Controls/Border/Border_Uniform_CornerRadius.expected.png