diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml b/samples/ControlCatalog/Pages/ComboBoxPage.xaml
index 025b85492c..d440b7cce3 100644
--- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml
+++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml
@@ -1,12 +1,20 @@
+ xmlns:sys="using:System"
+ xmlns:col="using:System.Collections">
ComboBoxA drop-down list.
-
+
+
+
+ Inline ItemsInline Item 2
@@ -14,6 +22,24 @@
Inline Item 4
+
+
+
+
+ Hello
+ World
+
+
+
+
+
+
+
+
+
+
+
+
@@ -46,7 +72,7 @@
-
+
diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs
index 2c7f34c5be..2f1cb2888e 100644
--- a/src/Avalonia.Base/Collections/AvaloniaList.cs
+++ b/src/Avalonia.Base/Collections/AvaloniaList.cs
@@ -280,8 +280,8 @@ namespace Avalonia.Collections
///
/// Gets a range of items from the collection.
///
- /// The first index to remove.
- /// The number of items to remove.
+ /// The zero-based index at which the range starts.
+ /// The number of elements in the range.
public IEnumerable GetRange(int index, int count)
{
return _inner.GetRange(index, count);
@@ -455,7 +455,7 @@ namespace Avalonia.Collections
}
///
- /// Ensures that the capacity of the list is at least .
+ /// Ensures that the capacity of the list is at least .
///
/// The capacity.
public void EnsureCapacity(int capacity)
diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs
index 9f2308a062..179ded3549 100644
--- a/src/Avalonia.Base/Utilities/TypeUtilities.cs
+++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs
@@ -93,13 +93,25 @@ namespace Avalonia.Utilities
return !type.IsValueType || IsNullableType(type);
}
+ ///
+ /// Returns a value indicating whether value can be casted to the specified type.
+ /// If value is null, checks if instances of that type can be null.
+ ///
+ /// The type to cast to
+ /// The value to check if cast possible
+ /// True if the cast is possible, otherwise false.
+ public static bool CanCast(object value)
+ {
+ return value is T || (value is null && AcceptsNull(typeof(T)));
+ }
+
///
/// Try to convert a value to a type by any means possible.
///
- /// The type to cast to.
- /// The value to cast.
+ /// The type to convert to.
+ /// The value to convert.
/// The culture to use.
- /// If successful, contains the cast value.
+ /// If successful, contains the convert value.
/// True if the cast was successful, otherwise false.
public static bool TryConvert(Type to, object value, CultureInfo culture, out object result)
{
@@ -216,10 +228,10 @@ namespace Avalonia.Utilities
/// Try to convert a value to a type using the implicit conversions allowed by the C#
/// language.
///
- /// The type to cast to.
- /// The value to cast.
- /// If successful, contains the cast value.
- /// True if the cast was successful, otherwise false.
+ /// The type to convert to.
+ /// The value to convert.
+ /// If successful, contains the converted value.
+ /// True if the convert was successful, otherwise false.
public static bool TryConvertImplicit(Type to, object value, out object result)
{
if (value == null)
@@ -278,8 +290,8 @@ namespace Avalonia.Utilities
/// Convert a value to a type by any means possible, returning the default for that type
/// if the value could not be converted.
///
- /// The value to cast.
- /// The type to cast to..
+ /// The value to convert.
+ /// The type to convert to..
/// The culture to use.
/// A value of .
public static object ConvertOrDefault(object value, Type type, CultureInfo culture)
@@ -291,8 +303,8 @@ namespace Avalonia.Utilities
/// Convert a value to a type using the implicit conversions allowed by the C# language or
/// return the default for the type if the value could not be converted.
///
- /// The value to cast.
- /// The type to cast to..
+ /// The value to convert.
+ /// The type to convert to.
/// A value of .
public static object ConvertImplicitOrDefault(object value, Type type)
{
diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs
index 9d8ebbfac1..587dd228a3 100644
--- a/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs
+++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs
@@ -83,8 +83,9 @@ namespace Avalonia.Collections
if (key == null)
key = item;
- if (_valueConverter != null)
- key = _valueConverter.Convert(key, typeof(object), level, culture);
+ var valueConverter = ValueConverter;
+ if (valueConverter != null)
+ key = valueConverter.Convert(key, typeof(object), level, culture);
return key;
}
@@ -99,6 +100,8 @@ namespace Avalonia.Collections
}
public override string PropertyName => _propertyPath;
+ public IValueConverter ValueConverter { get => _valueConverter; set => _valueConverter = value; }
+
private Type GetPropertyType(object o)
{
return o.GetType().GetNestedPropertyType(_propertyPath);
diff --git a/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs b/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs
index 6ac77fbb99..1d1a595ccf 100644
--- a/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs
+++ b/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs
@@ -1,10 +1,8 @@
using Avalonia.Data;
using Avalonia.Reactive;
using System;
-using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System.Reactive.Subjects;
-using System.Text;
namespace Avalonia.Controls.Utils
{
@@ -67,11 +65,14 @@ namespace Avalonia.Controls.Utils
private void SetSourceValue(object value)
{
- _settingSourceValue = true;
+ if (!_settingSourceValue)
+ {
+ _settingSourceValue = true;
- _sourceSubject.OnNext(value);
+ _sourceSubject.OnNext(value);
- _settingSourceValue = false;
+ _settingSourceValue = false;
+ }
}
private void SetControlValue(object value)
{
@@ -157,4 +158,4 @@ namespace Avalonia.Controls.Utils
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs
index 57c07916db..274696d501 100644
--- a/src/Avalonia.Controls/ComboBox.cs
+++ b/src/Avalonia.Controls/ComboBox.cs
@@ -1,6 +1,8 @@
using System;
using System.Linq;
+using System.Reactive.Disposables;
using Avalonia.Controls.Generators;
+using Avalonia.Controls.Mixins;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
@@ -80,7 +82,7 @@ namespace Avalonia.Controls
private bool _isDropDownOpen;
private Popup _popup;
private object _selectionBoxItem;
- private IDisposable _subscriptionsOnOpen;
+ private readonly CompositeDisposable _subscriptionsOnOpen = new CompositeDisposable();
///
/// Initializes static members of the class.
@@ -291,6 +293,7 @@ namespace Avalonia.Controls
_popup = e.NameScope.Get("PART_Popup");
_popup.Opened += PopupOpened;
+ _popup.Closed += PopupClosed;
}
internal void ItemFocused(ComboBoxItem dropDownItem)
@@ -303,8 +306,7 @@ namespace Avalonia.Controls
private void PopupClosed(object sender, EventArgs e)
{
- _subscriptionsOnOpen?.Dispose();
- _subscriptionsOnOpen = null;
+ _subscriptionsOnOpen.Clear();
if (CanFocus(this))
{
@@ -316,20 +318,34 @@ namespace Avalonia.Controls
{
TryFocusSelectedItem();
- _subscriptionsOnOpen?.Dispose();
- _subscriptionsOnOpen = null;
+ _subscriptionsOnOpen.Clear();
var toplevel = this.GetVisualRoot() as TopLevel;
if (toplevel != null)
{
- _subscriptionsOnOpen = toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) =>
+ toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) =>
{
//eat wheel scroll event outside dropdown popup while it's open
if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel)
{
ev.Handled = true;
}
- }, Interactivity.RoutingStrategies.Tunnel);
+ }, Interactivity.RoutingStrategies.Tunnel).DisposeWith(_subscriptionsOnOpen);
+ }
+
+ this.GetObservable(IsVisibleProperty).Subscribe(IsVisibleChanged).DisposeWith(_subscriptionsOnOpen);
+
+ foreach (var parent in this.GetVisualAncestors().OfType())
+ {
+ parent.GetObservable(IsVisibleProperty).Subscribe(IsVisibleChanged).DisposeWith(_subscriptionsOnOpen);
+ }
+ }
+
+ private void IsVisibleChanged(bool isVisible)
+ {
+ if (!isVisible && IsDropDownOpen)
+ {
+ IsDropDownOpen = false;
}
}
diff --git a/src/Avalonia.Controls/Mixins/DisposableMixin.cs b/src/Avalonia.Controls/Mixins/DisposableMixin.cs
new file mode 100644
index 0000000000..9b30b4ba4c
--- /dev/null
+++ b/src/Avalonia.Controls/Mixins/DisposableMixin.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Reactive.Disposables;
+
+namespace Avalonia.Controls.Mixins
+{
+ ///
+ /// Extension methods associated with the IDisposable interface.
+ ///
+ public static class DisposableMixin
+ {
+ ///
+ /// Ensures the provided disposable is disposed with the specified .
+ ///
+ ///
+ /// The type of the disposable.
+ ///
+ ///
+ /// The disposable we are going to want to be disposed by the CompositeDisposable.
+ ///
+ ///
+ /// The to which will be added.
+ ///
+ ///
+ /// The disposable.
+ ///
+ public static T DisposeWith(this T item, CompositeDisposable compositeDisposable)
+ where T : IDisposable
+ {
+ if (compositeDisposable is null)
+ {
+ throw new ArgumentNullException(nameof(compositeDisposable));
+ }
+
+ compositeDisposable.Add(item);
+ return item;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
index 984faa4d60..209feb351c 100644
--- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
+++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
@@ -115,7 +115,7 @@ namespace Avalonia.Controls.Platform
protected IMenu? Menu { get; private set; }
- protected static TimeSpan MenuShowDelay { get; } = TimeSpan.FromMilliseconds(400);
+ public static TimeSpan MenuShowDelay { get; set; } = TimeSpan.FromMilliseconds(400);
protected internal virtual void GotFocus(object sender, GotFocusEventArgs e)
{
diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
index 472727823a..1a46d84558 100644
--- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
@@ -327,7 +327,11 @@ namespace Avalonia.Controls.Presenters
var oldChild = Child;
var newChild = content as IControl;
- if (content != null && newChild == null)
+ // We want to allow creating Child from the Template, if Content is null.
+ // But it's important to not use DataTemplates, otherwise we will break content presenters in many places,
+ // otherwise it will blow up every ContentPresenter without Content set.
+ if (newChild == null
+ && (content != null || ContentTemplate != null))
{
var dataTemplate = this.FindDataTemplate(content, ContentTemplate) ??
(
diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs
index d5fb69a672..e804c4b4a9 100644
--- a/src/Avalonia.Controls/Primitives/Popup.cs
+++ b/src/Avalonia.Controls/Primitives/Popup.cs
@@ -2,6 +2,7 @@ using System;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Disposables;
+using Avalonia.Controls.Mixins;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives.PopupPositioning;
@@ -393,18 +394,8 @@ namespace Avalonia.Controls.Primitives
var handlerCleanup = new CompositeDisposable(5);
- void DeferCleanup(IDisposable? disposable)
- {
- if (disposable is null)
- {
- return;
- }
-
- handlerCleanup.Add(disposable);
- }
-
- DeferCleanup(popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty,
- HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty));
+ popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty,
+ HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty).DisposeWith(handlerCleanup);
popupHost.SetChild(Child);
((ISetLogicalParent)popupHost).SetParent(this);
@@ -418,19 +409,19 @@ namespace Avalonia.Controls.Primitives
PlacementConstraintAdjustment,
PlacementRect);
- DeferCleanup(SubscribeToEventHandler>(popupHost, RootTemplateApplied,
+ SubscribeToEventHandler>(popupHost, RootTemplateApplied,
(x, handler) => x.TemplateApplied += handler,
- (x, handler) => x.TemplateApplied -= handler));
+ (x, handler) => x.TemplateApplied -= handler).DisposeWith(handlerCleanup);
if (topLevel is Window window)
{
- DeferCleanup(SubscribeToEventHandler(window, WindowDeactivated,
+ SubscribeToEventHandler(window, WindowDeactivated,
(x, handler) => x.Deactivated += handler,
- (x, handler) => x.Deactivated -= handler));
+ (x, handler) => x.Deactivated -= handler).DisposeWith(handlerCleanup);
- DeferCleanup(SubscribeToEventHandler(window.PlatformImpl, WindowLostFocus,
+ SubscribeToEventHandler(window.PlatformImpl, WindowLostFocus,
(x, handler) => x.LostFocus += handler,
- (x, handler) => x.LostFocus -= handler));
+ (x, handler) => x.LostFocus -= handler).DisposeWith(handlerCleanup);
}
else
{
@@ -438,13 +429,13 @@ namespace Avalonia.Controls.Primitives
if (parentPopupRoot?.Parent is Popup popup)
{
- DeferCleanup(SubscribeToEventHandler>(popup, ParentClosed,
+ SubscribeToEventHandler>(popup, ParentClosed,
(x, handler) => x.Closed += handler,
- (x, handler) => x.Closed -= handler));
+ (x, handler) => x.Closed -= handler).DisposeWith(handlerCleanup);
}
}
- DeferCleanup(InputManager.Instance?.Process.Subscribe(ListenForNonClientClick));
+ InputManager.Instance?.Process.Subscribe(ListenForNonClientClick).DisposeWith(handlerCleanup);
var cleanupPopup = Disposable.Create((popupHost, handlerCleanup), state =>
{
@@ -466,17 +457,17 @@ namespace Avalonia.Controls.Primitives
dismissLayer.IsVisible = true;
dismissLayer.InputPassThroughElement = _overlayInputPassThroughElement;
- DeferCleanup(Disposable.Create(() =>
+ Disposable.Create(() =>
{
dismissLayer.IsVisible = false;
dismissLayer.InputPassThroughElement = null;
- }));
+ }).DisposeWith(handlerCleanup);
- DeferCleanup(SubscribeToEventHandler>(
+ SubscribeToEventHandler>(
dismissLayer,
PointerPressedDismissOverlay,
(x, handler) => x.PointerPressed += handler,
- (x, handler) => x.PointerPressed -= handler));
+ (x, handler) => x.PointerPressed -= handler).DisposeWith(handlerCleanup);
}
}
diff --git a/src/Avalonia.Controls/SystemDialog.cs b/src/Avalonia.Controls/SystemDialog.cs
index e74b950f23..d81926ecc7 100644
--- a/src/Avalonia.Controls/SystemDialog.cs
+++ b/src/Avalonia.Controls/SystemDialog.cs
@@ -4,30 +4,65 @@ using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Platform;
+#nullable enable
+
namespace Avalonia.Controls
{
+ ///
+ /// Base class for system file dialogs.
+ ///
public abstract class FileDialog : FileSystemDialog
{
+ ///
+ /// Gets or sets a collection of filters which determine the types of files displayed in an
+ /// or an .
+ ///
public List Filters { get; set; } = new List();
- public string InitialFileName { get; set; }
+
+ ///
+ /// Gets or sets initial file name that is displayed when the dialog is opened.
+ ///
+ public string? InitialFileName { get; set; }
}
+ ///
+ /// Base class for system file and directory dialogs.
+ ///
public abstract class FileSystemDialog : SystemDialog
{
[Obsolete("Use Directory")]
- public string InitialDirectory
+ public string? InitialDirectory
{
get => Directory;
set => Directory = value;
}
- public string Directory { get; set; }
+
+ ///
+ /// Gets or sets the initial directory that will be displayed when the file system dialog
+ /// is opened.
+ ///
+ public string? Directory { get; set; }
}
+ ///
+ /// Represents a system dialog that prompts the user to select a location for saving a file.
+ ///
public class SaveFileDialog : FileDialog
{
- public string DefaultExtension { get; set; }
+ ///
+ /// Gets or sets the default extension to be used to save the file (including the period ".").
+ ///
+ public string? DefaultExtension { get; set; }
- public async Task ShowAsync(Window parent)
+ ///
+ /// Shows the save file dialog.
+ ///
+ /// The parent window.
+ ///
+ /// A task that on completion contains the full path of the save location, or null if the
+ /// dialog was canceled.
+ ///
+ public async Task ShowAsync(Window parent)
{
if(parent == null)
throw new ArgumentNullException(nameof(parent));
@@ -37,11 +72,25 @@ namespace Avalonia.Controls
}
}
+ ///
+ /// Represents a system dialog that allows the user to select one or more files to open.
+ ///
public class OpenFileDialog : FileDialog
{
+ ///
+ /// Gets or sets a value indicating whether the user can select multiple files.
+ ///
public bool AllowMultiple { get; set; }
- public Task ShowAsync(Window parent)
+ ///
+ /// Shows the open file dialog.
+ ///
+ /// The parent window.
+ ///
+ /// A task that on completion returns an array containing the full path to the selected
+ /// files, or null if the dialog was canceled.
+ ///
+ public Task ShowAsync(Window parent)
{
if(parent == null)
throw new ArgumentNullException(nameof(parent));
@@ -49,15 +98,27 @@ namespace Avalonia.Controls
}
}
+ ///
+ /// Represents a system dialog that allows the user to select a directory.
+ ///
public class OpenFolderDialog : FileSystemDialog
{
[Obsolete("Use Directory")]
- public string DefaultDirectory
+ public string? DefaultDirectory
{
get => Directory;
set => Directory = value;
}
- public Task ShowAsync(Window parent)
+
+ ///
+ /// Shows the open folder dialog.
+ ///
+ /// The parent window.
+ ///
+ /// A task that on completion returns the full path of the selected directory, or null if the
+ /// dialog was canceled.
+ ///
+ public Task ShowAsync(Window parent)
{
if(parent == null)
throw new ArgumentNullException(nameof(parent));
@@ -65,14 +126,32 @@ namespace Avalonia.Controls
}
}
+
+ ///
+ /// Base class for system dialogs.
+ ///
public abstract class SystemDialog
{
- public string Title { get; set; }
+ ///
+ /// Gets or sets the dialog title.
+ ///
+ public string? Title { get; set; }
}
+ ///
+ /// Represents a filter in an or an .
+ ///
public class FileDialogFilter
{
- public string Name { get; set; }
+ ///
+ /// Gets or sets the name of the filter, e.g. ("Text files (.txt)").
+ ///
+ public string? Name { get; set; }
+
+ ///
+ /// Gets or sets a list of file extensions matched by the filter (e.g. "txt" or "*" for all
+ /// files).
+ ///
public List Extensions { get; set; } = new List();
}
}
diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs
index 4a6a1c6cfb..8e7b290247 100644
--- a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs
+++ b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs
@@ -1,5 +1,7 @@
using System;
+using Avalonia.Utilities;
+
namespace Avalonia.Controls.Templates
{
///
@@ -16,7 +18,7 @@ namespace Avalonia.Controls.Templates
///
/// Whether the control can be recycled.
public FuncDataTemplate(Func build, bool supportsRecycling = false)
- : base(typeof(T), CastBuild(build), supportsRecycling)
+ : base(o => TypeUtilities.CanCast(o), CastBuild(build), supportsRecycling)
{
}
@@ -63,7 +65,7 @@ namespace Avalonia.Controls.Templates
/// The weakly typed function.
private static Func