sobota, 21 sierpnia 2010

Problem PasswordBox przy wykorzystaniu wzorca MVVM

PasswordBox jak łatwo się domyślić jest standardową kontrolką WPF dzięki której użytkownik otrzymuje zamaskowane pole tekstowe do wpisywanie haseł itp. Kontrolka spisuje się wyśmienicie do czasu gdy przy budowaniu aplikacji nie wykorzystywany jest wzorzec MVVM (i inne pokrewne Model-View-* oparte o bindowaniu). Powód jest bardzo prosty. Ze względów bezpieczeństwa właściwość Password kontrolki nie jest uznawana jako Dependency Property co uniemożliwia bindowanie. Panowie z MS dokonali akurat takiego wyboru ponieważ wymusiłoby to ciągłe utrzymanie jawnego hasła w pamięci. W Internecie można spotkać się z różnymi pomysłami pokonania tego problemu. Od łamania zasad bezpieczeństwa (link) po łamanie zasad MVVM (link). Stąd moim głównym celem było takie zaprojektowanie rozwiązania, aby maksymalnie zminimalizować czas egzystencji czystego hasła w pamięci, a także zachować w pełni główną zaletę MVVM czyli testowalność. I tutaj przyszły z pomocą parametry bindowania.

Implementacja

Na sam początek potrzebny będzie interfejs za pośrednictwem którego otrzymamy hasło:
public interface IPlainPasswordProvider
{
   string Password { get; }
}
Można go teraz wykorzystać bezpośrednio w ViewModel dzięki czemu testowalność nie jest w żaden sposób zagrożona (co istotne jako framework MVVM użyłem MVVMF):
public class MainWindowViewModel : ObservableObject
{
    private RelayCommand<IPlainPasswordProvider> _okCommand = null;
    public ICommand OkCommand
    {
        get
        {
            if (_okCommand == null) {
                _okCommand = new RelayCommand<IPlainPasswordProvider>((provider) => {
                    OkCommandAction(provider);
                });
            }
            return _okCommand;
        }
    }

    public void OkCommandAction(IPlainPasswordProvider provider)
    {
        //dokonujemy operacji związanych z hasłem provider.Password
    }
}
Następnie dodajemy klasę implementującą interfejs IPlainPasswordProvider który będzie pobierała wartość SecureString bezpośrednio z PasswordBox i w "poprawny" sposób dekodowała:
public static class SecureStringExtension
{
    public static string ToUnsecureString(this SecureString secureString)
    {
        if (secureString == null) {
            throw new ArgumentNullException("secureString");
        }
        IntPtr unmenagedString = IntPtr.Zero;
        try {
            unmenagedString = Marshal.SecureStringToGlobalAllocUnicode(secureString);
            return Marshal.PtrToStringUni(unmenagedString);
        } finally {
            Marshal.ZeroFreeGlobalAllocUnicode(unmenagedString);
        }
    }
}

public class PasswordBoxPlainPasswordProvider : IPlainPasswordProvider
{
    private PasswordBox _passwordBox;

    public PasswordBoxPlainPasswordProvider(PasswordBox passwordBox)
    {
        if (passwordBox == null) {
            throw new ArgumentNullException("passwordBox");
        }
        _passwordBox = passwordBox;
    }

    public string Password
    {
        get
        {
            SecureString secureString = _passwordBox.SecurePassword;
            secureString.MakeReadOnly();
            return secureString.ToUnsecureString();
        }
    }
}
Kolejnym krokiem jest stworzenie kowertera imlementującego IValueConverter. Dzięki temu będzie on mógł być automatycznie wywołyny przez widok inicjalizując dostawcę instancją kontrolki PasswordBox.
public class PasswordBoxValueConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return new PasswordBoxPlainPasswordProvider(value as PasswordBox);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
W tym momencie mamy już wszystko czego potrzebujemy. Pozostaje jedynie napisać kod widoku który wykorzysta powyższe elementy:
<!-- 
    Tworzymy okno dodając dodatkową przestrzeń nazw xmlns:vm 
    w której znajdują się przygotowane wcześniej twory
-->
<Window x:Class="Revis.Example.WpfPasswordBox.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"        
        xmlns:vm="clr-namespace:Revis.Example.WpfPasswordBox.ViewModel"
        Title="PasswordBox Example" 
        Width="200" 
        Height="65"> 
    <Window.DataContext>
        <vm:MainWindowViewModel />
    </Window.DataContext>
    <StackPanel 
        Grid.Row="0"
        Orientation="Horizontal"
        >
        <Label>Hasło:</Label>
        <!-- PasswordBox z którego chcemy wyciągnąć informacje -->
        <PasswordBox
                x:Name="PasswordBoxName"
                Grid.Row="0"
                Width="100"
                />        
        <Button
            Grid.Row="1"
            Width="30"
            Command="{Binding OkCommand}"
            >
            <!-- Umieszczamy w słowniku zasobów przycisku konwerter -->
            <Button.Resources>
                <vm:PasswordBoxValueConverter x:Key="PasswordBoxValueConverter" />
            </Button.Resources>
            <!-- 
                Dodajemy parametr bindowania który przekaże do konwertera instancję
                PasswordBox o wskazanej nazwie
            -->
            <Button.CommandParameter>
                <Binding 
                    Converter="{StaticResource PasswordBoxValueConverter}" 
                    ElementName="PasswordBoxName"
                    />
            </Button.CommandParameter>
            Ok
        </Button>
    </StackPanel>
</Window>
I to już wszystko. Hasło trzymane jest w pamięci tylko wtedy kiedy jest potrzebne, a ViewModel wciąż służy nam zarówno po uruchomieniu jak i w testach aplikacji dzięki swojemu lekkiemu powiązaniu z widokiem.

3 komentarze:

  1. Bardzo fajne rozwiązanie:) W takiej postaci owszem działa, ale niestety kiedy chciałam je zastosować w okienku logowania poległam :( Mianowicie potrzebuję przesłać zarówno pole Hasło, jak i Login. W tym celu napisałam konwerter pozwalający na przesłanie dwóch parametrów, a pod drugi z nich podpięłam Twój konwerter.










    Niestety w takim przypadku Twój konwerter nie chce działać. Czy mógłbyś podać co należy robić w takim przypadku?

    OdpowiedzUsuń
  2. @Up
    Trudno powiedzieć, ale od strony MVVM po wywołaniu jakiejś akcji, aktualną wartość loginu powinieneś mieć w właściwości podpiętej pod jego textboxa. Stąd przy oknie logowania potrzebny jest tylko jeden konwerter dla hasła.

    OdpowiedzUsuń
  3. Wszystko działa. Dla mnie bomba. Pozdrawiam!

    OdpowiedzUsuń