Fix WPF Entry crashes when IsPassword=true (#8645)
* repro * fix * use DelayObfuscation fixes #8644
This commit is contained in:
Родитель
c7e2e822a1
Коммит
ccd693e137
|
@ -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;
|
||||
|
|
Загрузка…
Ссылка в новой задаче