Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e9388bf
Rewrite current UI test and add new UI test to test failing scenario
nicolaihenriksen Nov 4, 2022
4d9c233
Refactor PasswordBoxAssist to use behavior
nicolaihenriksen Nov 4, 2022
90471ba
Refactor PasswordBox reveal style TextBox focus- and text selection code
nicolaihenriksen Nov 4, 2022
0a9f4f8
Undo wrong change in TestBase
nicolaihenriksen Nov 4, 2022
bc349fa
Whitespace commit to force re-run
nicolaihenriksen Nov 4, 2022
bd746b4
Increase delay in test
nicolaihenriksen Nov 4, 2022
b282e6c
Rollback of increased test delay
nicolaihenriksen Nov 4, 2022
dda88f8
Update MaterialDesignThemes.Wpf/Behaviors/PasswordBoxRevealTextBoxBeh…
nicolaihenriksen Nov 5, 2022
7d32386
Update MaterialDesignThemes.Wpf/Behaviors/PasswordBoxRevealTextBoxBeh…
nicolaihenriksen Nov 5, 2022
e1ddcf8
Extract PasswordBoxBehavior into its own class
nicolaihenriksen Nov 5, 2022
847d6ab
Cache PropertyInfos and MethodInfos in PasswordBoxRevealTextBoxBehavior
nicolaihenriksen Nov 5, 2022
417c69c
Selecting minimum required version of dependency and updating nuspec
nicolaihenriksen Nov 5, 2022
4b78de6
Convert Password binding to opt-in feature
nicolaihenriksen Nov 6, 2022
3fdb918
File scoped namespace
nicolaihenriksen Nov 6, 2022
e89ce21
Behavior classes renames and change from internal to public
nicolaihenriksen Nov 6, 2022
21d7373
Use latest version of Xaml.Behaviors and update nuspec file
nicolaihenriksen Nov 6, 2022
da46c37
Attempting fix of pipeline
Keboo Nov 7, 2022
8124883
Adding screenshots for debugging
Keboo Nov 7, 2022
fac1639
Attempt at fixing failing UI test
nicolaihenriksen Nov 7, 2022
b52a9ce
Adding more screenshots for debugging
nicolaihenriksen Nov 7, 2022
100bad6
Activate test window on loaded event
nicolaihenriksen Nov 7, 2022
bfbd3d5
Yet another attempt at getting test window shown
nicolaihenriksen Nov 7, 2022
afbc730
Removing debugging screenshots
nicolaihenriksen Nov 7, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Refactor PasswordBoxAssist to use behavior
  • Loading branch information
nicolaihenriksen committed Nov 7, 2022
commit 4d9c23389bd16582276a68b65b4d2629e7107d57
3 changes: 2 additions & 1 deletion Directory.packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<PackageVersion Include="Microsoft.NETCore.Platforms" Version="6.0.6" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageVersion Include="Microsoft.Toolkit.MVVM" Version="7.1.2" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.37" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.1" />
<PackageVersion Include="Shouldly" Version="4.1.0" />
<PackageVersion Include="ShowMeTheXAML" Version="2.0.0" />
Expand All @@ -26,4 +27,4 @@
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" />
<PackageVersion Include="Xunit.StaFact" Version="1.1.11" />
</ItemGroup>
</Project>
</Project>
3 changes: 3 additions & 0 deletions MaterialDesignThemes.Wpf/MaterialDesignThemes.Wpf.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MaterialDesignColors.Wpf\MaterialDesignColors.Wpf.csproj" />
</ItemGroup>
Expand Down
82 changes: 53 additions & 29 deletions MaterialDesignThemes.Wpf/PasswordBoxAssist.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System.Windows.Data;
using System.Windows.Data;
using System.Windows.Documents;
using Microsoft.Xaml.Behaviors;

namespace MaterialDesignThemes.Wpf;

public static class PasswordBoxAssist
public class PasswordBoxAssist : Behavior<PasswordBox>
{
public static readonly DependencyProperty PasswordMaskedIconProperty = DependencyProperty.RegisterAttached(
"PasswordMaskedIcon", typeof(PackIconKind), typeof(PasswordBoxAssist), new FrameworkPropertyMetadata(PackIconKind.EyeOff, FrameworkPropertyMetadataOptions.Inherits));
Expand All @@ -24,45 +26,67 @@ public static class PasswordBoxAssist
public static void SetPassword(DependencyObject element, string value) => element.SetValue(PasswordProperty, value);
public static string GetPassword(DependencyObject element) => (string)element.GetValue(PasswordProperty);

// Internal attached DP used to initially wire up the connection between the masked PasswordBox content and the clear text TextBox content
internal static readonly DependencyProperty InitialPasswordProperty = DependencyProperty.RegisterAttached(
"InitialPassword", typeof(string), typeof(PasswordBoxAssist), new PropertyMetadata(default(string)));
internal static void SetInitialPassword(DependencyObject element, string value) => element.SetValue(InitialPasswordProperty, value);
internal static string GetInitialPassword(DependencyObject element) => (string)element.GetValue(InitialPasswordProperty);
private static readonly DependencyProperty IsChangingProperty = DependencyProperty.RegisterAttached(
"IsChanging", typeof(bool), typeof(PasswordBoxAssist), new UIPropertyMetadata(false));
private static void SetIsChanging(UIElement element, bool value) => element.SetValue(IsChangingProperty, value);
private static bool GetIsChanging(UIElement element) => (bool)element.GetValue(IsChangingProperty);

private static readonly DependencyProperty SelectionProperty = DependencyProperty.RegisterAttached(
"Selection", typeof(TextSelection), typeof(PasswordBoxAssist), new UIPropertyMetadata(default(TextSelection)));
private static void SetSelection(DependencyObject obj, TextSelection? value) => obj.SetValue(SelectionProperty, value);
private static TextSelection? GetSelection(DependencyObject obj) => (TextSelection?)obj.GetValue(SelectionProperty);

private static readonly DependencyProperty IsPasswordInitializedProperty = DependencyProperty.RegisterAttached(
"IsPasswordInitialized", typeof(bool), typeof(PasswordBoxAssist), new PropertyMetadata(false));
private static readonly DependencyProperty RevealedPasswordTextBoxProperty = DependencyProperty.RegisterAttached(
"RevealedPasswordTextBox", typeof(TextBox), typeof(PasswordBoxAssist), new UIPropertyMetadata(default(TextBox)));
private static void SetRevealedPasswordTextBox(DependencyObject obj, TextBox? value) => obj.SetValue(RevealedPasswordTextBoxProperty, value);
private static TextBox? GetRevealedPasswordTextBox(DependencyObject obj) => (TextBox?)obj.GetValue(RevealedPasswordTextBoxProperty);

private static readonly DependencyProperty SettingPasswordProperty = DependencyProperty.RegisterAttached(
"SettingPassword", typeof(bool), typeof(PasswordBoxAssist), new PropertyMetadata(false));

private static void HandlePasswordChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
/// <summary>
/// Handles changes to the 'Password' attached property.
/// </summary>
private static void HandlePasswordChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (d is not PasswordBox passwordBox)
return;

if ((bool)passwordBox.GetValue(SettingPasswordProperty))
return;
if (sender is PasswordBox targetPasswordBox)
{
targetPasswordBox.PasswordChanged -= PasswordBoxPasswordChanged;
if (!GetIsChanging(targetPasswordBox))
{
targetPasswordBox.Password = (string)e.NewValue;
}
targetPasswordBox.PasswordChanged += PasswordBoxPasswordChanged;
}
}

if (!(bool)passwordBox.GetValue(IsPasswordInitializedProperty))
/// <summary>
/// Handle the 'PasswordChanged'-event on the PasswordBox
/// </summary>
private static void PasswordBoxPasswordChanged(object sender, RoutedEventArgs e)
{
if (sender is PasswordBox passwordBox)
{
passwordBox.SetValue(IsPasswordInitializedProperty, true);
WeakEventManager<PasswordBox, RoutedEventArgs>.AddHandler(passwordBox, nameof(PasswordBox.PasswordChanged), HandlePasswordChanged);
SetIsChanging(passwordBox, true);
SetPassword(passwordBox, passwordBox.Password);
SetIsChanging(passwordBox, false);
}
passwordBox.Password = e.NewValue as string;
}

private static void HandlePasswordChanged(object? sender, RoutedEventArgs e)
private void PasswordBoxLoaded(object sender, RoutedEventArgs e) => SetPassword(AssociatedObject, AssociatedObject.Password);

protected override void OnAttached()
{
if (sender is not PasswordBox passwordBox)
return;
base.OnAttached();
AssociatedObject.PasswordChanged += PasswordBoxPasswordChanged;
AssociatedObject.Loaded += PasswordBoxLoaded;
}

passwordBox.SetValue(SettingPasswordProperty, true);
string currentPassword = GetPassword(passwordBox);
if (currentPassword != passwordBox.Password)
protected override void OnDetaching()
{
if (AssociatedObject != null)
{
SetPassword(passwordBox, passwordBox.Password);
AssociatedObject.Loaded -= PasswordBoxLoaded;
AssociatedObject.PasswordChanged -= PasswordBoxPasswordChanged;
}
passwordBox.SetValue(SettingPasswordProperty, false);
base.OnDetaching();
}
}
8 changes: 8 additions & 0 deletions MaterialDesignThemes.Wpf/StylizedBehaviorCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Microsoft.Xaml.Behaviors;

namespace MaterialDesignThemes.Wpf;

public class StylizedBehaviorCollection : FreezableCollection<Behavior>
{
protected override Freezable CreateInstanceCore() => new StylizedBehaviorCollection();
}
122 changes: 122 additions & 0 deletions MaterialDesignThemes.Wpf/StylizedBehaviors.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
using Microsoft.Xaml.Behaviors;

namespace MaterialDesignThemes.Wpf;

internal class StylizedBehaviors
{
private static readonly DependencyProperty OriginalBehaviorProperty = DependencyProperty.RegisterAttached(
"OriginalBehavior", typeof(Behavior), typeof(StylizedBehaviors), new UIPropertyMetadata(null));
private static void SetOriginalBehavior(DependencyObject obj, Behavior? value) => obj.SetValue(OriginalBehaviorProperty, value);
private static Behavior? GetOriginalBehavior(DependencyObject obj) => (Behavior?)obj.GetValue(OriginalBehaviorProperty);

internal static readonly DependencyProperty BehaviorsProperty = DependencyProperty.RegisterAttached(
"Behaviors", typeof(StylizedBehaviorCollection), typeof(StylizedBehaviors), new FrameworkPropertyMetadata(null, OnPropertyChanged));
internal static void SetBehaviors(DependencyObject uie, StylizedBehaviorCollection? value) => uie.SetValue(BehaviorsProperty, value);
internal static StylizedBehaviorCollection? GetBehaviors(DependencyObject uie) => (StylizedBehaviorCollection?)uie.GetValue(BehaviorsProperty);

private static void OnPropertyChanged(DependencyObject dpo, DependencyPropertyChangedEventArgs e)
{
if (dpo is not FrameworkElement frameworkElement)
{
return;
}

var newBehaviors = e.NewValue as StylizedBehaviorCollection;
var oldBehaviors = e.OldValue as StylizedBehaviorCollection;
if (newBehaviors == oldBehaviors)
{
return;
}

var itemBehaviors = Interaction.GetBehaviors(frameworkElement);
frameworkElement.Unloaded -= FrameworkElementUnloaded;
if (oldBehaviors != null)
{
foreach (var behavior in oldBehaviors)
{
int index = GetIndexOf(itemBehaviors, behavior);
if (index >= 0)
{
itemBehaviors.RemoveAt(index);
}
}
}

if (newBehaviors != null)
{
foreach (var behavior in newBehaviors)
{
int index = GetIndexOf(itemBehaviors, behavior);
if (index < 0)
{
var clone = (Behavior)behavior.Clone();
SetOriginalBehavior(clone, behavior);
itemBehaviors.Add(clone);
}
}
}

if (itemBehaviors.Count > 0)
{
frameworkElement.Unloaded += FrameworkElementUnloaded;
}
}

private static void FrameworkElementUnloaded(object sender, RoutedEventArgs e)
{
// BehaviorCollection doesn't call Detach, so we do this
if (sender is not FrameworkElement frameworkElement)
{
return;
}

var itemBehaviors = Interaction.GetBehaviors(frameworkElement);
foreach (var behavior in itemBehaviors)
{
behavior.Detach();
}

frameworkElement.Loaded += FrameworkElementLoaded;
}

private static void FrameworkElementLoaded(object sender, RoutedEventArgs e)
{
if (sender is not FrameworkElement frameworkElement)
{
return;
}

frameworkElement.Loaded -= FrameworkElementLoaded;
var itemBehaviors = Interaction.GetBehaviors(frameworkElement);
foreach (var behavior in itemBehaviors)
{
behavior.Attach(frameworkElement);
}
}

private static int GetIndexOf(BehaviorCollection itemBehaviors, Behavior behavior)
{
int index = -1;

var originalBehavior = GetOriginalBehavior(behavior);

for (int i = 0; i < itemBehaviors.Count; i++)
{
var currentBehavior = itemBehaviors[i];
if (currentBehavior == behavior || currentBehavior == originalBehavior)
{
index = i;
break;
}

var currentOriginalBehavior = GetOriginalBehavior(currentBehavior);
if (currentOriginalBehavior == behavior || currentOriginalBehavior == originalBehavior)
{
index = i;
break;
}
}

return index;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:MaterialDesignThemes.Wpf.Converters"
xmlns:internal="clr-namespace:MaterialDesignThemes.Wpf.Internal"
xmlns:wpf="clr-namespace:MaterialDesignThemes.Wpf">
xmlns:behaviors="http://schemas.microsoft.com/xaml/behaviors"
xmlns:wpf="clr-namespace:MaterialDesignThemes.Wpf"
xmlns:internalbehaviors="clr-namespace:MaterialDesignThemes.Wpf.Behaviors">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.ValidationErrorTemplate.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Menu.xaml" />
Expand Down Expand Up @@ -443,6 +445,13 @@
<Setter Property="wpf:TextFieldAssist.UnderlineBrush" Value="{DynamicResource PrimaryHueMidBrush}" />
<Setter Property="wpf:TextFieldAssist.CharacterCounterStyle" Value="{StaticResource MaterialDesignPasswordCharacterCounterTextBlock}" />
<Setter Property="wpf:TextFieldAssist.CharacterCounterVisibility" Value="Hidden" />
<Setter Property="wpf:StylizedBehaviors.Behaviors">
<Setter.Value>
<wpf:StylizedBehaviorCollection>
<wpf:PasswordBoxAssist />
</wpf:StylizedBehaviorCollection>
</Setter.Value>
</Setter>
</Style>

<Style x:Key="MaterialDesignFloatingHintPasswordBox"
Expand Down Expand Up @@ -946,13 +955,18 @@
<Setter Property="internal:ClearText.HandlesClearCommand" Value="True" />
<Setter Property="wpf:HintAssist.Foreground" Value="{DynamicResource PrimaryHueMidBrush}" />
<Setter Property="wpf:HintAssist.HelperTextStyle" Value="{StaticResource MaterialDesignPasswordHelperTextBlock}" />
<Setter Property="wpf:PasswordBoxAssist.InitialPassword" Value="" />
<Setter Property="wpf:PasswordBoxAssist.IsPasswordRevealed" Value="False" />
<Setter Property="wpf:PasswordBoxAssist.Password" Value="{Binding RelativeSource={RelativeSource Self}, Path=(wpf:PasswordBoxAssist.InitialPassword)}" />
<Setter Property="wpf:TextFieldAssist.TextBoxViewMargin" Value="{x:Static wpf:Constants.DefaultTextBoxViewMargin}" />
<Setter Property="wpf:TextFieldAssist.UnderlineBrush" Value="{DynamicResource PrimaryHueMidBrush}" />
<Setter Property="wpf:TextFieldAssist.CharacterCounterStyle" Value="{StaticResource MaterialDesignPasswordCharacterCounterTextBlock}" />
<Setter Property="wpf:TextFieldAssist.CharacterCounterVisibility" Value="Hidden" />
<Setter Property="wpf:StylizedBehaviors.Behaviors">
<Setter.Value>
<wpf:StylizedBehaviorCollection>
<wpf:PasswordBoxAssist />
</wpf:StylizedBehaviorCollection>
</Setter.Value>
</Setter>
</Style>

<Style x:Key="MaterialDesignFloatingHintRevealPasswordBox"
Expand Down