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 e7d82ae25e..d3053a6af1 100644
--- a/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs
+++ b/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs
@@ -19,14 +19,13 @@ namespace Avalonia.MonoMac
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
@@ -57,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
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);
+ }
+ }
}
}