diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index c241466aea..bda660be51 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -223,11 +223,11 @@ namespace Avalonia.Controls.Presenters if (PasswordChar != default(char)) { - result = base.CreateFormattedText(constraint, new string(PasswordChar, Text.Length)); + result = base.CreateFormattedText(constraint, new string(PasswordChar, text?.Length ?? 0)); } else { - result = base.CreateFormattedText(constraint, Text); + result = base.CreateFormattedText(constraint, text); } var selectionStart = SelectionStart; diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 1fee309360..e7cf666589 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -111,6 +111,8 @@ namespace Avalonia.Controls /// public TextBlock() { + _text = string.Empty; + Observable.Merge( this.GetObservable(TextProperty).Select(_ => Unit.Default), this.GetObservable(TextAlignmentProperty).Select(_ => Unit.Default), diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index e45f30f818..464e4188d2 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -34,25 +34,25 @@ namespace Avalonia.Controls /// Defines the ToolTip.Placement property. /// public static readonly AttachedProperty PlacementProperty = - AvaloniaProperty.RegisterAttached("Placement", defaultValue: PlacementMode.Pointer); + AvaloniaProperty.RegisterAttached("Placement", defaultValue: PlacementMode.Pointer); /// /// Defines the ToolTip.HorizontalOffset property. /// public static readonly AttachedProperty HorizontalOffsetProperty = - AvaloniaProperty.RegisterAttached("HorizontalOffset"); + AvaloniaProperty.RegisterAttached("HorizontalOffset"); /// /// Defines the ToolTip.VerticalOffset property. /// public static readonly AttachedProperty VerticalOffsetProperty = - AvaloniaProperty.RegisterAttached("VerticalOffset", 20); + AvaloniaProperty.RegisterAttached("VerticalOffset", 20); /// /// Defines the ToolTip.ShowDelay property. /// public static readonly AttachedProperty ShowDelayProperty = - AvaloniaProperty.RegisterAttached("ShowDelay", 400); + AvaloniaProperty.RegisterAttached("ShowDelay", 400); /// /// Stores the curernt instance in the control. diff --git a/src/Markup/Avalonia.Markup/Data/Parsers/ArgumentListParser.cs b/src/Markup/Avalonia.Markup/Data/Parsers/ArgumentListParser.cs index 563b372c78..05b3b29be0 100644 --- a/src/Markup/Avalonia.Markup/Data/Parsers/ArgumentListParser.cs +++ b/src/Markup/Avalonia.Markup/Data/Parsers/ArgumentListParser.cs @@ -51,10 +51,10 @@ namespace Avalonia.Markup.Data.Parsers } } - throw new ExpressionParseException(r.Position, "Expected ']'."); + throw new ExpressionParseException(r.Position, $"Expected '{close}'."); } - return null; + throw new ExpressionParseException(r.Position, $"Expected '{open}'."); } } } diff --git a/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionParser.cs index 93ddf77376..f31f6eccb7 100644 --- a/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionParser.cs @@ -40,6 +40,10 @@ namespace Avalonia.Markup.Data.Parsers case State.AttachedProperty: state = ParseAttachedProperty(r, nodes); break; + + case State.Indexer: + state = ParseIndexer(r, nodes); + break; } } @@ -67,6 +71,10 @@ namespace Avalonia.Markup.Data.Parsers { return State.AttachedProperty; } + else if (PeekOpenBracket(r)) + { + return State.Indexer; + } else { var identifier = IdentifierParser.Parse(r); @@ -92,20 +100,9 @@ namespace Avalonia.Markup.Data.Parsers nodes.Add(new StreamNode()); return State.AfterMember; } - else + else if (PeekOpenBracket(r)) { - var args = ArgumentListParser.Parse(r, '[', ']'); - - if (args != null) - { - if (args.Count == 0) - { - throw new ExpressionParseException(r.Position, "Indexer may not be empty."); - } - - nodes.Add(new IndexerNode(args)); - return State.AfterMember; - } + return State.Indexer; } return State.End; @@ -151,6 +148,19 @@ namespace Avalonia.Markup.Data.Parsers return State.AfterMember; } + private State ParseIndexer(Reader r, List nodes) + { + var args = ArgumentListParser.Parse(r, '[', ']'); + + if (args.Count == 0) + { + throw new ExpressionParseException(r.Position, "Indexer may not be empty."); + } + + nodes.Add(new IndexerNode(args)); + return State.AfterMember; + } + private static bool ParseNot(Reader r) { return !r.End && r.TakeIf('!'); @@ -166,6 +176,11 @@ namespace Avalonia.Markup.Data.Parsers return !r.End && r.TakeIf('('); } + private static bool PeekOpenBracket(Reader r) + { + return !r.End && r.Peek == '['; + } + private static bool ParseStreamOperator(Reader r) { return !r.End && r.TakeIf('^'); @@ -177,6 +192,7 @@ namespace Avalonia.Markup.Data.Parsers AfterMember, BeforeMember, AttachedProperty, + Indexer, End, } } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs new file mode 100644 index 0000000000..23dae8f341 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs @@ -0,0 +1,40 @@ +using Avalonia.Controls.Presenters; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Presenters +{ + public class TextPresenter_Tests + { + [Fact] + public void TextPresenter_Can_Contain_Null_With_Password_Char_Set() + { + var target = new TextPresenter + { + PasswordChar = '*' + }; + + Assert.NotNull(target.FormattedText); + } + + [Fact] + public void TextPresenter_Can_Contain_Null_WithOut_Password_Char_Set() + { + var target = new TextPresenter(); + + Assert.NotNull(target.FormattedText); + } + + [Fact] + public void Text_Presenter_Replaces_Formatted_Text_With_Password_Char() + { + var target = new TextPresenter + { + PasswordChar = '*', + Text = "Test" + }; + + Assert.NotNull(target.FormattedText); + Assert.Equal("****", target.FormattedText.Text); + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs index b8886456dd..9a1140fc05 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs @@ -15,5 +15,15 @@ namespace Avalonia.Controls.UnitTests BindingMode.OneWay, TextBlock.TextProperty.GetMetadata(typeof(TextBlock)).DefaultBindingMode); } + + [Fact] + public void Default_Text_Value_Should_Be_EmptyString() + { + var textBlock = new TextBlock(); + + Assert.Equal( + "", + textBlock.Text); + } } } diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs index a68213baee..1bdbf0d895 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs @@ -338,6 +338,18 @@ namespace Avalonia.Markup.UnitTests.Data GC.KeepAlive(data); } + [Fact] + public async Task Indexer_Only_Binding_Works() + { + var data = new[] { 1, 2, 3 }; + + var target = new ExpressionObserver(data, "[1]"); + + var value = await target.Take(1); + + Assert.Equal(data[1], value); + } + private class NonIntegerIndexer : NotifyingBase { private readonly Dictionary _storage = new Dictionary();