Fix WPF Entry crashes when IsPassword=true (#8645)

* repro

* fix

* use DelayObfuscation
fixes #8644
This commit is contained in:
melimion 2020-01-02 22:06:51 +03:00 коммит произвёл Samantha Houts
Родитель c7e2e822a1
Коммит ccd693e137
4 изменённых файлов: 99 добавлений и 37 удалений

Просмотреть файл

@ -0,0 +1,54 @@
using System.ComponentModel;
using Xamarin.Forms.CustomAttributes;
using Xamarin.Forms.Internals;
namespace Xamarin.Forms.Controls.Issues
{
[Preserve(AllMembers = true)]
[Issue(IssueTracker.Github, 8644, "WPF Entry crashes when IsPassword=true", PlatformAffected.WPF)]
public class Issue8644 : TestContentPage
{
public class BinCon : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
string _title;
public string Title
{
get => _title;
set
{
_title = value?.Length > 4 ? value.Substring(0, 4) : value;
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Title)));
}
}
}
protected override void Init()
{
var bc = new BinCon();
var e1 = new Entry
{
BindingContext = bc,
Margin = new Thickness(50),
HorizontalOptions = LayoutOptions.FillAndExpand,
IsPassword = true,
};
e1.SetBinding(Entry.TextProperty, nameof(BinCon.Title));
// Label just to show current Entry text, not needed for test
var lbl = new Label { BindingContext = bc };
lbl.SetBinding(Label.TextProperty, nameof(BinCon.Title));
var stack = new StackLayout
{
Children = {
new Label { Text = "Type more than 4 symbols" },
e1,
lbl,
}
};
Content = stack;
}
}
}

Просмотреть файл

@ -22,6 +22,7 @@
<Compile Include="$(MSBuildThisFileDirectory)CollectionViewHeaderFooterTemplate.cs" />
<Compile Include="$(MSBuildThisFileDirectory)CollectionViewHeaderFooterView.cs" />
<Compile Include="$(MSBuildThisFileDirectory)CollectionViewItemsUpdatingScrollMode.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue8644.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue8177.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue7773.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue4606.cs" />

Просмотреть файл

@ -26,10 +26,10 @@ namespace Xamarin.Forms.Platform.WPF
public static readonly DependencyProperty IsPasswordProperty = DependencyProperty.Register("IsPassword", typeof(bool), typeof(FormsTextBox),
new PropertyMetadata(default(bool), OnIsPasswordChanged));
public new static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(FormsTextBox),
public new static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(FormsTextBox),
new PropertyMetadata("", TextPropertyChanged));
protected internal static readonly DependencyProperty DisabledTextProperty = DependencyProperty.Register("DisabledText", typeof(string), typeof(FormsTextBox),
protected internal static readonly DependencyProperty DisabledTextProperty = DependencyProperty.Register("DisabledText", typeof(string), typeof(FormsTextBox),
new PropertyMetadata(""));
static InputScope s_passwordInputScope;
@ -52,8 +52,8 @@ namespace Xamarin.Forms.Platform.WPF
public string PlaceholderText
{
get { return (string)GetValue (PlaceholderTextProperty); }
set { SetValue (PlaceholderTextProperty, value); }
get { return (string)GetValue(PlaceholderTextProperty); }
set { SetValue(PlaceholderTextProperty, value); }
}
public Brush PlaceholderForegroundBrush
@ -88,11 +88,12 @@ namespace Xamarin.Forms.Platform.WPF
return s_passwordInputScope;
}
}
void DelayObfuscation()
{
int lengthDifference = base.Text.Length - Text.Length;
var savedSelectionStart = SelectionStart;
string updatedRealText = DetermineTextFromPassword(Text, SelectionStart, base.Text);
if (Text == updatedRealText)
@ -101,7 +102,9 @@ namespace Xamarin.Forms.Platform.WPF
return;
}
_internalChangeFlag = true;
Text = updatedRealText;
_internalChangeFlag = false;
// Cancel any pending delayed obfuscation
_cts?.Cancel();
@ -118,10 +121,10 @@ namespace Xamarin.Forms.Platform.WPF
else
{
// Only one character was added; we need to leave it visible for a brief time period
// Obfuscate all but the last character for now
newText = Obfuscate(Text, true);
// Obfuscate all but the character added for now
newText = Obfuscate(Text, savedSelectionStart - 1);
// Leave the last character visible until a new character is added
// Leave the added character visible until a new character is added
// or sufficient time has passed
if (_cts == null)
{
@ -134,19 +137,20 @@ namespace Xamarin.Forms.Platform.WPF
_cts.Token.ThrowIfCancellationRequested();
await Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() =>
{
var ss = SelectionStart;
var sl = SelectionLength;
base.Text = Obfuscate(Text);
SelectionStart = base.Text.Length;
SelectionStart = ss;
SelectionLength = sl;
}));
}, _cts.Token);
}
if (base.Text == newText)
if (base.Text != newText)
{
return;
base.Text = newText;
}
base.Text = newText;
SelectionStart = base.Text.Length;
SelectionStart = savedSelectionStart;
}
static string DetermineTextFromPassword(string realText, int start, string passwordText)
@ -164,14 +168,19 @@ namespace Xamarin.Forms.Platform.WPF
return sb.ToString();
}
string Obfuscate(string text, bool leaveLastVisible = false)
string Obfuscate(string text, int visibleSymbolIndex = -1)
{
if (!leaveLastVisible)
if (visibleSymbolIndex == -1)
return new string(ObfuscationCharacter, text?.Length ?? 0);
return text == null || text.Length == 1
? text
: new string(ObfuscationCharacter, text.Length - 1) + text.Substring(text.Length - 1, 1);
if (text == null || text.Length == 1)
return text;
var prefix = visibleSymbolIndex > 0 ? new string(ObfuscationCharacter, visibleSymbolIndex) : string.Empty;
var suffix = visibleSymbolIndex == text.Length - 1
? string.Empty
: new string(ObfuscationCharacter, text.Length - visibleSymbolIndex - 1);
return prefix + text.Substring(visibleSymbolIndex, 1) + suffix;
}
static void OnIsPasswordChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
@ -198,7 +207,7 @@ namespace Xamarin.Forms.Platform.WPF
// The ctrlDown flag is used to track if the Ctrl key is pressed; if it's actively being used and the most recent
// key to trigger OnKeyDown, then treat it as handled.
var ctrlDown = (e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl) && e.IsDown;
// The shift, tab, and directional (Home/End/PgUp/PgDown included) keys can be used to select text and should otherwise
// be ignored.
if (
@ -234,28 +243,22 @@ namespace Xamarin.Forms.Platform.WPF
base.OnKeyDown(e);
if (_cachedSelectionLength > 0 && !ctrlDown)
{
var savedSelectionStart = SelectionStart;
Text = Text.Remove(SelectionStart, _cachedSelectionLength);
SelectionStart = savedSelectionStart;
}
}
}
else
base.OnKeyDown(e);
}
void OnTextChanged(object sender, System.Windows.Controls.TextChangedEventArgs textChangedEventArgs)
{
if (IsPassword)
{
string updatedRealText = DetermineTextFromPassword(Text, SelectionStart, base.Text);
string updatedText = Obfuscate(updatedRealText);
var savedSelectionStart = SelectionStart;
if (Text != updatedRealText)
Text = updatedRealText;
if (base.Text != updatedText)
base.Text = updatedText;
SelectionStart = savedSelectionStart;
DelayObfuscation();
}
else if (base.Text != Text)
{
@ -272,11 +275,11 @@ namespace Xamarin.Forms.Platform.WPF
{
if (_internalChangeFlag)
return;
var savedSelectionStart = SelectionStart;
base.Text = IsPassword ? Obfuscate(Text) : Text;
DisabledText = base.Text;
SelectionStart = base.Text.Length;
var len = base.Text.Length;
SelectionStart = savedSelectionStart > len ? len : savedSelectionStart;
}
static void TextPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)

Просмотреть файл

@ -108,13 +108,17 @@ namespace Xamarin.Forms.Platform.WPF
((IElementController)Element).SetValueFromRenderer(Entry.TextProperty, Control.Text);
// If an Entry.TextChanged handler modified the value of the Entry's text, the values could now be
// out-of-sync; re-sync them and force the TextBox cursor to the end of the text
// out-of-sync; re-sync them and fix TextBox cursor position
string entryText = Element.Text;
if (Control.Text != entryText)
{
Control.Text = entryText;
if (Control.Text != null)
Control.SelectionStart = Control.Text.Length;
{
var savedSelectionStart = Control.SelectionStart;
var len = Control.Text.Length;
Control.SelectionStart = savedSelectionStart > len ? len : savedSelectionStart;
}
}
_ignoreTextChange = false;