diff --git a/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs b/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs
new file mode 100644
index 0000000000..fc695762b8
--- /dev/null
+++ b/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Globalization;
+
+namespace Avalonia.Data.Converters
+{
+ ///
+ /// A value converter which calls
+ ///
+ public class StringFormatValueConverter : IValueConverter
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The format string.
+ ///
+ /// An optional inner converter to be called before the format takes place.
+ ///
+ public StringFormatValueConverter(string format, IValueConverter inner)
+ {
+ Contract.Requires(format != null);
+
+ Format = format;
+ Inner = inner;
+ }
+
+ ///
+ /// Gets an inner value converter which will be called before the string format takes place.
+ ///
+ public IValueConverter Inner { get; }
+
+ ///
+ /// Gets the format string.
+ ///
+ public string Format { get; }
+
+ ///
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ value = Inner?.Convert(value, targetType, parameter, culture) ?? value;
+ return string.Format(culture, Format, value);
+ }
+
+ ///
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotSupportedException("Two way bindings are not supported with a string format");
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs
index 457a7bd4b4..0ae4be5550 100644
--- a/src/Avalonia.Controls/Primitives/PopupRoot.cs
+++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs
@@ -83,21 +83,22 @@ namespace Avalonia.Controls.Primitives
///
public void SnapInsideScreenEdges()
{
- var window = this.GetSelfAndLogicalAncestors().OfType().First();
-
- var screen = window.Screens.ScreenFromPoint(Position);
+ var screen = Application.Current.MainWindow?.Screens.ScreenFromPoint(Position);
- var screenX = Position.X + Bounds.Width - screen.Bounds.X;
- var screenY = Position.Y + Bounds.Height - screen.Bounds.Y;
-
- if (screenX > screen.Bounds.Width)
+ if (screen != null)
{
- Position = Position.WithX(Position.X - (screenX - screen.Bounds.Width));
- }
+ var screenX = Position.X + Bounds.Width - screen.Bounds.X;
+ var screenY = Position.Y + Bounds.Height - screen.Bounds.Y;
- if (screenY > screen.Bounds.Height)
- {
- Position = Position.WithY(Position.Y - (screenY - screen.Bounds.Height));
+ if (screenX > screen.Bounds.Width)
+ {
+ Position = Position.WithX(Position.X - (screenX - screen.Bounds.Width));
+ }
+
+ if (screenY > screen.Bounds.Height)
+ {
+ Position = Position.WithY(Position.Y - (screenY - screen.Bounds.Height));
+ }
}
}
diff --git a/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj b/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj
index f80462e958..0aed0a9717 100644
--- a/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj
+++ b/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj
@@ -10,6 +10,7 @@
+
diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
index 08ba26573f..1ceef5c824 100644
--- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
@@ -43,8 +43,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
Path = Path,
Priority = Priority,
Source = Source,
+ StringFormat = StringFormat,
RelativeSource = RelativeSource,
- DefaultAnchor = new WeakReference(GetDefaultAnchor((ITypeDescriptorContext)serviceProvider))
+ DefaultAnchor = new WeakReference(GetDefaultAnchor(descriptorContext))
};
}
@@ -79,6 +80,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
public object Source { get; set; }
+ public string StringFormat { get; set; }
+
public RelativeSource RelativeSource { get; set; }
}
}
diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs
index 03678c3b5a..4f18c682b4 100644
--- a/src/Markup/Avalonia.Markup/Data/Binding.cs
+++ b/src/Markup/Avalonia.Markup/Data/Binding.cs
@@ -84,6 +84,11 @@ namespace Avalonia.Data
///
public object Source { get; set; }
+ ///
+ /// Gets or sets the string format.
+ ///
+ public string StringFormat { get; set; }
+
public WeakReference DefaultAnchor { get; set; }
///
@@ -181,11 +186,23 @@ namespace Avalonia.Data
fallback = null;
}
+ var converter = Converter;
+ var targetType = targetProperty?.PropertyType ?? typeof(object);
+
+ // We only respect `StringFormat` if the type of the property we're assigning to will
+ // accept a string. Note that this is slightly different to WPF in that WPF only applies
+ // `StringFormat` for target type `string` (not `object`).
+ if (!string.IsNullOrWhiteSpace(StringFormat) &&
+ (targetType == typeof(string) || targetType == typeof(object)))
+ {
+ converter = new StringFormatValueConverter(StringFormat, converter);
+ }
+
var subject = new BindingExpression(
observer,
- targetProperty?.PropertyType ?? typeof(object),
+ targetType,
fallback,
- Converter ?? DefaultValueConverter.Instance,
+ converter ?? DefaultValueConverter.Instance,
ConverterParameter,
Priority);
diff --git a/src/OSX/Avalonia.MonoMac/PopupImpl.cs b/src/OSX/Avalonia.MonoMac/PopupImpl.cs
index ba4b7f0eac..267e47a6a0 100644
--- a/src/OSX/Avalonia.MonoMac/PopupImpl.cs
+++ b/src/OSX/Avalonia.MonoMac/PopupImpl.cs
@@ -8,11 +8,23 @@ namespace Avalonia.MonoMac
public PopupImpl()
{
UpdateStyle();
+ Window.Level = NSWindowLevel.PopUpMenu;
}
protected override NSWindowStyle GetStyle()
{
return NSWindowStyle.Borderless;
}
+
+ protected override CustomWindow CreateCustomWindow() => new CustomPopupWindow(this);
+
+ private class CustomPopupWindow : CustomWindow
+ {
+ public CustomPopupWindow(WindowBaseImpl impl)
+ : base(impl)
+ { }
+
+ public override bool WorksWhenModal() => true;
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs b/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs
index 89cef59b53..d3053a6af1 100644
--- a/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs
+++ b/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs
@@ -14,17 +14,18 @@ namespace Avalonia.MonoMac
private readonly ManagedWindowResizeDragHelper _managedDrag;
public CustomWindow Window { get; private set; }
+ private bool _closed;
+
public WindowBaseImpl()
{
_managedDrag = new ManagedWindowResizeDragHelper(this, _ => { }, ResizeForManagedDrag);
- Window = new CustomWindow(this)
- {
- StyleMask = NSWindowStyle.Titled,
- BackingType = NSBackingStore.Buffered,
- ContentView = View,
- // ReSharper disable once VirtualMemberCallInConstructor
- Delegate = CreateWindowDelegate()
- };
+ // ReSharper disable once VirtualMemberCallInConstructor
+ Window = CreateCustomWindow();
+ Window.StyleMask = NSWindowStyle.Titled;
+ Window.BackingType = NSBackingStore.Buffered;
+ Window.ContentView = View;
+ // ReSharper disable once VirtualMemberCallInConstructor
+ Window.Delegate = CreateWindowDelegate();
}
public class CustomWindow : NSWindow
@@ -55,6 +56,7 @@ namespace Avalonia.MonoMac
public void SetCanBecomeKeyAndMain() => _canBecomeKeyAndMain = true;
}
+ protected virtual CustomWindow CreateCustomWindow() => new CustomWindow(this);
protected virtual NSWindowDelegate CreateWindowDelegate() => new WindowBaseDelegate(this);
public class WindowBaseDelegate : NSWindowDelegate
@@ -78,9 +80,8 @@ namespace Avalonia.MonoMac
public override void WillClose(global::MonoMac.Foundation.NSNotification notification)
{
- _impl.Window.Dispose();
- _impl.Window = null;
- _impl.Dispose();
+ _impl._closed = true;
+ _impl.Closed?.Invoke();
}
public override CGRect WillUseStandardFrame(NSWindow window, CGRect newFrame)
@@ -94,8 +95,19 @@ namespace Avalonia.MonoMac
{
return true;
}
+
+ public override void DidResize(NSNotification notification)
+ {
+ _impl.OnResized();
+ }
}
+ ///
+ /// As you can't combine NSWindowDelegate overrides and events this is a workaround
+ ///
+ protected virtual void OnResized()
+ {
+ }
public Point Position
{
@@ -185,12 +197,11 @@ namespace Avalonia.MonoMac
return cocoaScreenPoint.ConvertPointY().ToAvaloniaPoint();
}
-
-
public override void Dispose()
{
- Window?.Close();
- Window?.Dispose();
+ if (!_closed)
+ Window.Close();
+ Window.Dispose();
base.Dispose();
}
}
diff --git a/src/OSX/Avalonia.MonoMac/WindowImpl.cs b/src/OSX/Avalonia.MonoMac/WindowImpl.cs
index 3900af8709..89b5550688 100644
--- a/src/OSX/Avalonia.MonoMac/WindowImpl.cs
+++ b/src/OSX/Avalonia.MonoMac/WindowImpl.cs
@@ -26,18 +26,19 @@ namespace Avalonia.MonoMac
});
Window.SetCanBecomeKeyAndMain();
-
- Window.DidResize += delegate
- {
- var windowState = Window.IsMiniaturized ? WindowState.Minimized
- : (IsZoomed ? WindowState.Maximized : WindowState.Normal);
+ }
- if (windowState != _lastWindowState)
- {
- _lastWindowState = windowState;
- WindowStateChanged?.Invoke(windowState);
- }
- };
+
+ protected override void OnResized()
+ {
+ var windowState = Window.IsMiniaturized ? WindowState.Minimized
+ : (IsZoomed ? WindowState.Maximized : WindowState.Normal);
+
+ if (windowState != _lastWindowState)
+ {
+ _lastWindowState = windowState;
+ WindowStateChanged?.Invoke(windowState);
+ }
}
public WindowState WindowState
@@ -129,24 +130,47 @@ namespace Avalonia.MonoMac
class ModalDisposable : IDisposable
{
readonly WindowImpl _impl;
+ readonly IntPtr _modalSession;
+ bool disposed;
- public ModalDisposable(WindowImpl impl)
+ public ModalDisposable(WindowImpl impl, IntPtr modalSession)
{
_impl = impl;
+ _modalSession = modalSession;
+ }
+
+ public void Continue()
+ {
+ if (disposed)
+ return;
+
+ var response = (NSRunResponse)NSApplication.SharedApplication.RunModalSession(_modalSession);
+ if (response == NSRunResponse.Continues)
+ {
+ Dispatcher.UIThread.Post(Continue, DispatcherPriority.ContextIdle);
+ }
+ else
+ {
+ Logging.Logger.Log(Logging.LogEventLevel.Debug, "MonoMac", this, "Modal session ended");
+ }
}
public void Dispose()
{
+ Logging.Logger.Log(Logging.LogEventLevel.Debug, "MonoMac", this, "ModalDisposable disposed");
_impl.Window.OrderOut(_impl.Window);
+ NSApplication.SharedApplication.EndModalSession(_modalSession);
+ disposed = true;
}
}
public IDisposable ShowDialog()
{
- //TODO: Investigate how to return immediately.
- // May be add some magic to our run loop or something
- NSApplication.SharedApplication.RunModalForWindow(Window);
- return new ModalDisposable(this);
+ var session = NSApplication.SharedApplication.BeginModalSession(Window);
+ var disposable = new ModalDisposable(this, session);
+ Dispatcher.UIThread.Post(disposable.Continue, DispatcherPriority.ContextIdle);
+
+ return disposable;
}
}
-}
\ No newline at end of file
+}
diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
index 94d5caa720..66f0cc3a40 100644
--- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
@@ -64,7 +64,10 @@ namespace Avalonia.Controls.UnitTests
{
ContextMenu = sut
};
- new Window { Content = target };
+
+ var window = new Window { Content = target };
+
+ Avalonia.Application.Current.MainWindow = window;
target.RaiseEvent(new PointerReleasedEventArgs
{
diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs
new file mode 100644
index 0000000000..123aadfda5
--- /dev/null
+++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs
@@ -0,0 +1,146 @@
+// 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.Controls;
+using Avalonia.Data;
+using Avalonia.Data.Converters;
+using Avalonia.Data.Core;
+using Xunit;
+
+namespace Avalonia.Markup.UnitTests.Data
+{
+ public class BindingTests_Converters
+ {
+ [Fact]
+ public void Converter_Should_Be_Used()
+ {
+ var textBlock = new TextBlock
+ {
+ DataContext = new Class1(),
+ };
+
+ var target = new Binding(nameof(Class1.Foo))
+ {
+ Converter = StringConverters.NullOrEmpty,
+ };
+
+ var expressionObserver = (BindingExpression)target.Initiate(
+ textBlock,
+ TextBlock.TextProperty).Observable;
+
+ Assert.Same(StringConverters.NullOrEmpty, expressionObserver.Converter);
+ }
+
+ public class When_Binding_To_String
+ {
+ [Fact]
+ public void StringFormatConverter_Should_Be_Used_When_Binding_Has_StringFormat()
+ {
+ var textBlock = new TextBlock
+ {
+ DataContext = new Class1(),
+ };
+
+ var target = new Binding(nameof(Class1.Foo))
+ {
+ StringFormat = "Hello {0}",
+ };
+
+ var expressionObserver = (BindingExpression)target.Initiate(
+ textBlock,
+ TextBlock.TextProperty).Observable;
+
+ Assert.IsType(expressionObserver.Converter);
+ }
+ }
+
+ public class When_Binding_To_Object
+ {
+ [Fact]
+ public void StringFormatConverter_Should_Be_Used_When_Binding_Has_StringFormat()
+ {
+ var textBlock = new TextBlock
+ {
+ DataContext = new Class1(),
+ };
+
+ var target = new Binding(nameof(Class1.Foo))
+ {
+ StringFormat = "Hello {0}",
+ };
+
+ var expressionObserver = (BindingExpression)target.Initiate(
+ textBlock,
+ TextBlock.TagProperty).Observable;
+
+ Assert.IsType(expressionObserver.Converter);
+ }
+ }
+
+ public class When_Binding_To_Non_String_Or_Object
+ {
+ [Fact]
+ public void StringFormatConverter_Should_Not_Be_Used_When_Binding_Has_StringFormat()
+ {
+ var textBlock = new TextBlock
+ {
+ DataContext = new Class1(),
+ };
+
+ var target = new Binding(nameof(Class1.Foo))
+ {
+ StringFormat = "Hello {0}",
+ };
+
+ var expressionObserver = (BindingExpression)target.Initiate(
+ textBlock,
+ TextBlock.MarginProperty).Observable;
+
+ Assert.Same(DefaultValueConverter.Instance, expressionObserver.Converter);
+ }
+ }
+
+ [Fact]
+ public void StringFormat_Should_Be_Applied()
+ {
+ var textBlock = new TextBlock
+ {
+ DataContext = new Class1(),
+ };
+
+ var target = new Binding(nameof(Class1.Foo))
+ {
+ StringFormat = "Hello {0}",
+ };
+
+ textBlock.Bind(TextBlock.TextProperty, target);
+
+ Assert.Equal("Hello foo", textBlock.Text);
+ }
+
+ [Fact]
+ public void StringFormat_Should_Be_Applied_After_Converter()
+ {
+ var textBlock = new TextBlock
+ {
+ DataContext = new Class1(),
+ };
+
+ var target = new Binding(nameof(Class1.Foo))
+ {
+ Converter = StringConverters.NotNullOrEmpty,
+ StringFormat = "Hello {0}",
+ };
+
+ textBlock.Bind(TextBlock.TextProperty, target);
+
+ Assert.Equal("Hello True", textBlock.Text);
+ }
+
+ private class Class1
+ {
+ public string Foo { get; set; } = "foo";
+ }
+ }
+}
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
index f327e9ccf2..fef9dfb675 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
@@ -308,5 +308,27 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal(5.6, AttachedPropertyOwner.GetDouble(textBlock));
}
}
+
+ [Fact]
+ public void Binding_To_TextBlock_Text_With_StringConverter_Works()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var xaml = @"
+
+
+";
+ var loader = new AvaloniaXamlLoader();
+ var window = (Window)loader.Load(xaml);
+ var textBlock = window.FindControl("textBlock");
+
+ textBlock.DataContext = new { Foo = "world" };
+ window.ApplyTemplate();
+
+ Assert.Equal("Hello world", textBlock.Text);
+ }
+ }
}
}