diff --git a/samples/ControlCatalog/Converter/MathSubtractConverter.cs b/samples/ControlCatalog/Converter/MathSubtractConverter.cs
new file mode 100644
index 0000000000..009a56c0ec
--- /dev/null
+++ b/samples/ControlCatalog/Converter/MathSubtractConverter.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+
+namespace ControlCatalog.Converter;
+
+public class MathSubtractConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return (double)value - (double)parameter;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotSupportedException();
+ }
+}
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index facce2aa82..f3b52428ca 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -145,6 +145,9 @@
+
+
+
diff --git a/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml b/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml
new file mode 100644
index 0000000000..a410577d27
--- /dev/null
+++ b/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 8
+
+
+
+
+ The TransitioningContentControl control allows you to show a page transition whenever the Content changes.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml.cs b/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml.cs
new file mode 100644
index 0000000000..2851a6c890
--- /dev/null
+++ b/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml.cs
@@ -0,0 +1,19 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace ControlCatalog.Pages
+{
+ public partial class TransitioningContentControlPage : UserControl
+ {
+ public TransitioningContentControlPage()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs b/samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs
new file mode 100644
index 0000000000..b092a07f4a
--- /dev/null
+++ b/samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs
@@ -0,0 +1,309 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Animation;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+using Avalonia.Styling;
+using Avalonia.VisualTree;
+using MiniMvvm;
+
+namespace ControlCatalog.ViewModels
+{
+ public class TransitioningContentControlPageViewModel : ViewModelBase
+ {
+ public TransitioningContentControlPageViewModel()
+ {
+ var assetLoader = AvaloniaLocator.Current.GetService();
+
+ var images = new string[]
+ {
+ "delicate-arch-896885_640.jpg",
+ "hirsch-899118_640.jpg",
+ "maple-leaf-888807_640.jpg"
+ };
+
+ foreach (var image in images)
+ {
+ var path = $"avares://ControlCatalog/Assets/{image}";
+ Images.Add(new Bitmap(assetLoader.Open(new Uri(path))));
+ }
+
+ SetupTransitions();
+
+ SelectedTransition = PageTransitions[1];
+ SelectedImage = Images[0];
+ }
+
+ public List PageTransitions { get; } = new List();
+
+ public List Images { get; } = new List();
+
+
+ private Bitmap? _SelectedImage;
+
+ ///
+ /// Gets or Sets the selected image
+ ///
+ public Bitmap? SelectedImage
+ {
+ get { return _SelectedImage; }
+ set { this.RaiseAndSetIfChanged(ref _SelectedImage, value); }
+ }
+
+
+ private PageTransition _SelectedTransition;
+
+ ///
+ /// Gets or sets the transition to play
+ ///
+ public PageTransition SelectedTransition
+ {
+ get { return _SelectedTransition; }
+ set { this.RaiseAndSetIfChanged(ref _SelectedTransition, value); }
+ }
+
+
+
+ private bool _ClipToBounds;
+
+ ///
+ /// Gets or sets if the content should be clipped to bounds
+ ///
+ public bool ClipToBounds
+ {
+ get { return _ClipToBounds; }
+ set { this.RaiseAndSetIfChanged(ref _ClipToBounds, value); }
+ }
+
+
+ private int _Duration = 500;
+
+ ///
+ /// Gets or Sets the duration
+ ///
+ public int Duration
+ {
+ get { return _Duration; }
+ set
+ {
+ this.RaiseAndSetIfChanged(ref _Duration , value);
+ SetupTransitions();
+ }
+ }
+
+ private void SetupTransitions()
+ {
+ if (PageTransitions.Count == 0)
+ {
+ PageTransitions.AddRange(new[]
+ {
+ new PageTransition("None"),
+ new PageTransition("CrossFade"),
+ new PageTransition("Slide horizontally"),
+ new PageTransition("Slide vertically"),
+ new PageTransition("Composite"),
+ new PageTransition("Custom")
+ });
+ }
+
+ PageTransitions[1].Transition = new CrossFade(TimeSpan.FromMilliseconds(Duration));
+ PageTransitions[2].Transition = new PageSlide(TimeSpan.FromMilliseconds(Duration), PageSlide.SlideAxis.Horizontal);
+ PageTransitions[3].Transition = new PageSlide(TimeSpan.FromMilliseconds(Duration), PageSlide.SlideAxis.Vertical);
+
+ var compositeTransition = new CompositePageTransition();
+ compositeTransition.PageTransitions.Add(PageTransitions[1].Transition);
+ compositeTransition.PageTransitions.Add(PageTransitions[2].Transition);
+ compositeTransition.PageTransitions.Add(PageTransitions[3].Transition);
+ PageTransitions[4].Transition = compositeTransition;
+
+ PageTransitions[5].Transition = new CustomTransition(TimeSpan.FromMilliseconds(Duration));
+ }
+
+ public void NextImage()
+ {
+ var index = Images.IndexOf(SelectedImage) + 1;
+
+ if (index >= Images.Count)
+ {
+ index = 0;
+ }
+
+ SelectedImage = Images[index];
+ }
+
+ public void PrevImage()
+ {
+ var index = Images.IndexOf(SelectedImage) - 1;
+
+ if (index < 0)
+ {
+ index = Images.Count-1;
+ }
+
+ SelectedImage = Images[index];
+ }
+ }
+
+ public class PageTransition : ViewModelBase
+ {
+ public PageTransition(string displayTitle)
+ {
+ DisplayTitle = displayTitle;
+ }
+
+ public string DisplayTitle { get; }
+
+
+ private IPageTransition _Transition;
+
+ ///
+ /// Gets or sets the transition
+ ///
+ public IPageTransition Transition
+ {
+ get { return _Transition; }
+ set { this.RaiseAndSetIfChanged(ref _Transition, value); }
+ }
+
+ public override string ToString()
+ {
+ return DisplayTitle;
+ }
+
+ }
+
+ public class CustomTransition : IPageTransition
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CustomTransition()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The duration of the animation.
+ public CustomTransition(TimeSpan duration)
+ {
+ Duration = duration;
+ }
+
+ ///
+ /// Gets the duration of the animation.
+ ///
+ public TimeSpan Duration { get; set; }
+
+ public async Task Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return;
+ }
+
+ var tasks = new List();
+ var parent = GetVisualParent(from, to);
+ var scaleProperty = ScaleTransform.ScaleYProperty;
+
+ if (from != null)
+ {
+ var animation = new Animation
+ {
+ FillMode = FillMode.Forward,
+ Children =
+ {
+ new KeyFrame
+ {
+ Setters = { new Setter { Property = scaleProperty, Value = 1d } },
+ Cue = new Cue(0d)
+ },
+ new KeyFrame
+ {
+ Setters =
+ {
+ new Setter
+ {
+ Property = scaleProperty,
+ Value = 0d
+ }
+ },
+ Cue = new Cue(1d)
+ }
+ },
+ Duration = Duration
+ };
+ tasks.Add(animation.RunAsync(from, null, cancellationToken));
+ }
+
+ if (to != null)
+ {
+ to.IsVisible = true;
+ var animation = new Animation
+ {
+ FillMode = FillMode.Forward,
+ Children =
+ {
+ new KeyFrame
+ {
+ Setters =
+ {
+ new Setter
+ {
+ Property = scaleProperty,
+ Value = 0d
+ }
+ },
+ Cue = new Cue(0d)
+ },
+ new KeyFrame
+ {
+ Setters = { new Setter { Property = scaleProperty, Value = 1d } },
+ Cue = new Cue(1d)
+ }
+ },
+ Duration = Duration
+ };
+ tasks.Add(animation.RunAsync(to, null, cancellationToken));
+ }
+
+ await Task.WhenAll(tasks);
+
+ if (from != null && !cancellationToken.IsCancellationRequested)
+ {
+ from.IsVisible = false;
+ }
+ }
+
+ ///
+ /// Gets the common visual parent of the two control.
+ ///
+ /// The from control.
+ /// The to control.
+ /// The common parent.
+ ///
+ /// The two controls do not share a common parent.
+ ///
+ ///
+ /// Any one of the parameters may be null, but not both.
+ ///
+ private static IVisual GetVisualParent(IVisual? from, IVisual? to)
+ {
+ var p1 = (from ?? to)!.VisualParent;
+ var p2 = (to ?? from)!.VisualParent;
+
+ if (p1 != null && p2 != null && p1 != p2)
+ {
+ throw new ArgumentException("Controls for PageSlide must have same parent.");
+ }
+
+ return p1 ?? throw new InvalidOperationException("Cannot determine visual parent.");
+ }
+ }
+}
diff --git a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
index 1aa7f8ea04..e0dfa49a44 100644
--- a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
+++ b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
@@ -22,7 +22,7 @@
40
- 200
+ 220
36
36
32
diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs
index 5353e8175d..4e47a720f0 100644
--- a/src/Avalonia.Base/Data/BindingOperations.cs
+++ b/src/Avalonia.Base/Data/BindingOperations.cs
@@ -99,14 +99,15 @@ namespace Avalonia.Data
private sealed class TwoWayBindingDisposable : IDisposable
{
- private readonly IDisposable _first;
- private readonly IDisposable _second;
+ private readonly IDisposable _toTargetSubscription;
+ private readonly IDisposable _fromTargetSubsription;
+
private bool _isDisposed;
- public TwoWayBindingDisposable(IDisposable first, IDisposable second)
+ public TwoWayBindingDisposable(IDisposable toTargetSubscription, IDisposable fromTargetSubsription)
{
- _first = first;
- _second = second;
+ _toTargetSubscription = toTargetSubscription;
+ _fromTargetSubsription = fromTargetSubsription;
}
public void Dispose()
@@ -116,8 +117,8 @@ namespace Avalonia.Data
return;
}
- _first.Dispose();
- _second.Dispose();
+ _fromTargetSubsription.Dispose();
+ _toTargetSubscription.Dispose();
_isDisposed = true;
}
diff --git a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs
index 5948d81810..a923c44d05 100644
--- a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs
+++ b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs
@@ -2,6 +2,8 @@
using System.Globalization;
using System.Linq;
using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Media;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives
@@ -58,18 +60,17 @@ namespace Avalonia.Controls.Primitives
private Vector _offset;
private bool _hasInit;
private bool _suppressUpdateOffset;
- private ListBoxItem? _pressedItem;
public DateTimePickerPanel()
{
FormatDate = DateTime.Now;
- AddHandler(ListBoxItem.PointerPressedEvent, OnItemPointerDown, Avalonia.Interactivity.RoutingStrategies.Bubble);
- AddHandler(ListBoxItem.PointerReleasedEvent, OnItemPointerUp, Avalonia.Interactivity.RoutingStrategies.Bubble);
+ AddHandler(TappedEvent, OnItemTapped, RoutingStrategies.Bubble);
}
static DateTimePickerPanel()
{
FocusableProperty.OverrideDefaultValue(true);
+ BackgroundProperty.OverrideDefaultValue(Brushes.Transparent);
AffectsMeasure(ItemHeightProperty);
}
@@ -523,26 +524,13 @@ namespace Avalonia.Controls.Primitives
return newValue;
}
- private void OnItemPointerDown(object? sender, PointerPressedEventArgs e)
+ private void OnItemTapped(object? sender, TappedEventArgs e)
{
- if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed &&
- e.Source is IVisual source)
- {
- _pressedItem = GetItemFromSource(source);
- e.Handled = true;
- }
- }
-
- private void OnItemPointerUp(object? sender, PointerReleasedEventArgs e)
- {
- if (e.GetCurrentPoint(this).Properties.PointerUpdateKind == PointerUpdateKind.LeftButtonReleased &&
- _pressedItem != null &&
- e.Source is IVisual source &&
- GetItemFromSource(source) is ListBoxItem item &&
- item.Tag is int tag)
+ if (e.Source is IVisual source &&
+ GetItemFromSource(source) is ListBoxItem listBoxItem &&
+ listBoxItem.Tag is int tag)
{
SelectedValue = tag;
- _pressedItem = null;
e.Handled = true;
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/BindingSetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/BindingSetterViewModel.cs
new file mode 100644
index 0000000000..de3e56e514
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/BindingSetterViewModel.cs
@@ -0,0 +1,51 @@
+using System;
+using Avalonia.Data;
+using Avalonia.Markup.Xaml.MarkupExtensions;
+using Avalonia.Media;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+ internal class BindingSetterViewModel : SetterViewModel
+ {
+ public BindingSetterViewModel(AvaloniaProperty property, object? value) : base(property, value)
+ {
+ switch (value)
+ {
+ case Binding binding:
+ Path = binding.Path;
+ Tint = Brushes.CornflowerBlue;
+
+ break;
+ case CompiledBindingExtension binding:
+ Path = binding.Path.ToString();
+ Tint = Brushes.DarkGreen;
+
+ break;
+ case TemplateBinding binding:
+ if (binding.Property is AvaloniaProperty templateProperty)
+ {
+ Path = $"{templateProperty.OwnerType.Name}.{templateProperty.Name}";
+ }
+ else
+ {
+ Path = "Unassigned";
+ }
+
+ Tint = Brushes.OrangeRed;
+
+ break;
+ default:
+ throw new ArgumentException("Invalid binding type", nameof(value));
+ }
+ }
+
+ public IBrush Tint { get; }
+
+ public string Path { get; }
+
+ public override void CopyValue()
+ {
+ CopyToClipboard(Path);
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
index 8f0a4d07b0..701947d2ab 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
@@ -8,6 +8,7 @@ using System.Reflection;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
+using Avalonia.Data;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Styling;
using Avalonia.VisualTree;
@@ -87,7 +88,16 @@ namespace Avalonia.Diagnostics.ViewModels
}
else
{
- setterVm = new SetterViewModel(regularSetter.Property, setterValue);
+ var isBinding = IsBinding(setterValue);
+
+ if (isBinding)
+ {
+ setterVm = new BindingSetterViewModel(regularSetter.Property, setterValue);
+ }
+ else
+ {
+ setterVm = new SetterViewModel(regularSetter.Property, setterValue);
+ }
}
setters.Add(setterVm);
@@ -117,6 +127,19 @@ namespace Avalonia.Diagnostics.ViewModels
return null;
}
+ private bool IsBinding(object? value)
+ {
+ switch (value)
+ {
+ case Binding:
+ case CompiledBindingExtension:
+ case TemplateBinding:
+ return true;
+ }
+
+ return false;
+ }
+
public TreePageViewModel TreePage { get; }
public DataGridCollectionView? PropertiesView
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs
index 38cbefcb93..559ed49911 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs
@@ -34,7 +34,7 @@ namespace Avalonia.Diagnostics.ViewModels
IsVisible = true;
}
- public void CopyValue()
+ public virtual void CopyValue()
{
var textToCopy = Value?.ToString();
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
index d7acbbd577..ec97b213a9 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
@@ -138,6 +138,26 @@
+
+
+
+
+
+
+
+
+
+
+
+ {
+
+
+ }
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml
index 857d70084c..7e9f5d9429 100644
--- a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml
@@ -101,23 +101,17 @@
IsVisible="False"
HorizontalAlignment="Right" />
-
-
-
-
-
-
+
-
@@ -195,8 +188,8 @@
-
@@ -213,8 +206,8 @@
-
@@ -226,8 +219,8 @@
-
diff --git a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
index c82757799d..095a2ef51f 100644
--- a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
@@ -1,4 +1,6 @@
-
+
+ IsChecked="{Binding $parent[TextBox].RevealPassword, Mode=TwoWay}" />
diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
index 7c4c5c96a5..d4e14d00b8 100644
--- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
+++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
@@ -829,6 +829,30 @@ namespace Avalonia.Base.UnitTests
subscription.Dispose();
}
+ [Fact]
+ public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation_With_Value()
+ {
+ var target = new Class1();
+ var source = new TestTwoWayBindingViewModel() { Value = 1 };
+ source.ResetSetterCalled();
+
+ target.Bind(Class1.DoubleValueProperty, new Binding(nameof(source.Value), BindingMode.TwoWay) { Source = source });
+
+ Assert.False(source.SetterCalled);
+ }
+
+ [Fact]
+ public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation_Indexer_With_Value()
+ {
+ var target = new Class1();
+ var source = new TestTwoWayBindingViewModel() { [0] = 1 };
+ source.ResetSetterCalled();
+
+ target.Bind(Class1.DoubleValueProperty, new Binding("[0]", BindingMode.TwoWay) { Source = source });
+
+ Assert.False(source.SetterCalled);
+ }
+
///
/// Returns an observable that returns a single value but does not complete.
///
@@ -943,6 +967,11 @@ namespace Avalonia.Base.UnitTests
}
public bool SetterCalled { get; private set; }
+
+ public void ResetSetterCalled()
+ {
+ SetterCalled = false;
+ }
}
}
}