Mittwoch, 4. Februar 2015

Dialoge in WPF with MVVM

Mein aktueller Stand zu einem einfachen DialogService mit WPF und MVVM. Ausgangspunkt war meine Frage auf stackoverflow: http://stackoverflow.com/questions/3801681/good-or-bad-practice-for-dialogs-in-wpf-with-mvvm.

Der Aufruf im Viewmodel:

var result = _dialogservice.ShowDialog("Titel für den Dialog", viewmodel4Dialog);
if(result.HasValue && result.Value)
{
    
}
Damit man diesen Aufruf einfach in einem Viemodel absetzen kann, habe ich folgende Komponenten erstellt. IUIDialogWindowService



/// <summary>
/// Interface für das Anzeigen von Dialogen
/// </summary>
public interface IUIDialogWindowService
{
    ///<summary>
    /// Zeigt ein Dialog an.
    ///</summary>
    ///<param name="titel">Titel für den Dialog</param>
    ///<param name="datacontext">DataContext für den Dialog</param>
    ///<returns>true wenn DialogResult=true, ansonsten false</returns>
    bool? ShowDialog(string titel, object datacontext);
 
    ///<summary>
    /// Zeigt ein Dialog an.
    ///</summary>
    ///<param name="titel">Titel für den Dialog</param>
    ///<param name="datacontext">DataContext für den Dialog</param>
    ///<param name="minHeigth">Minimum Height</param>
    ///<param name="minWidth">Minimum Width</param>
    ///<param name="maxHeigth">Maximum Height</param>
    ///<param name="maxWidth">Maximum Width</param>
    ///<returns>true wenn DialogResult=true, ansonsten false</returns>
    bool? ShowDialog(string titel, object datacontext, double minHeigth = 0, double minWidth=0, double maxHeigth = double.PositiveInfinity, double maxWidth = double.PositiveInfinity);
 
 
    /// <summary>
    /// Zeigt ein Dialog an
    /// </summary>
    /// <param name="titel">Titel für den Dialog<</param>
    /// <param name="datacontext">DataContext für den Dialog</param>
    /// <param name="settings">ApplicationsSetting für Height and Width</param>
    /// <param name="pathHeigthSetting">Name für Height Setting</param>
    /// <param name="pathWidthSetting">Name für Width Setting</param>
    /// <param name="minHeigth">Minimum Height</param>
    /// <param name="minWidth">Minimum Width</param>
    /// <param name="maxHeigth">Maximum Height</param>
    /// <param name="maxWidth">Maximum Width</param>
    /// <returns>true wenn DialogResult=true, ansonsten false</returns>
    bool? ShowDialog(string titel, object datacontext, ApplicationSettingsBase settings, string pathHeigthSetting, string pathWidthSetting, double minHeigth = 0, double minWidth = 0, double maxHeigth = double.PositiveInfinity, double maxWidth = double.PositiveInfinity);
}
   

WpfUIDialogWindowService mit MEF Attributen(kann man auch weglassen)

[PartCreationPolicy(CreationPolicy.Shared)]
[Export(typeof(IUIDialogWindowService))]
public class WpfUIDialogWindowService : IUIDialogWindowService
{
        #region Implementation of IUIDialogWindowService
 
    ///<summary>
    /// Zeigt ein Dialog an.
    ///</summary>
    ///<param name="titel">Titel für den Dialog</param>
    ///<param name="datacontext">DataContext für den Dialog</param>
    ///<returns>true wenn DialogResult=true, ansonsten false</returns>
    public bool? ShowDialog(string titel, object datacontext)
    {
        var win = new DialogWindow {Title = titel, DataContext = datacontext};
        win.Owner = Application.Current.MainWindow;
        win.ShowInTaskbar = false;
        return win.ShowDialog();
    }
 
    ///<summary>
    /// Zeigt ein Dialog an.
    ///</summary>
    ///<param name="titel">Titel für den Dialog</param>
    ///<param name="datacontext">DataContext für den Dialog</param>
    ///<param name="minHeigth">Minimum Height</param>
    ///<param name="minWidth">Minimum Width</param>
    ///<param name="maxHeigth">Maximum Height</param>
    ///<param name="maxWidth">Maximum Width</param>
    ///<returns>true wenn DialogResult=true, ansonsten false</returns>
    public bool? ShowDialog(string titel, object datacontext, double minHeigth = 0, double minWidth = 0, double maxHeigth = double.PositiveInfinity, double maxWidth = double.PositiveInfinity)
    {
        var win = new DialogWindow { Title = titel, DataContext = datacontext };
        
        win.Owner = Application.Current.MainWindow;
        win.ShowInTaskbar = false;
        win.MinHeight = minHeigth;
        win.MinWidth = minWidth;
        win.MaxHeight = maxHeigth;
        win.MaxWidth = maxWidth;
 
        return win.ShowDialog();
    }
 
    /// <summary>
    /// Zeigt ein Dialog an
    /// </summary>
    /// <param name="titel">Titel für den Dialog<</param>
    /// <param name="datacontext">DataContext für den Dialog</param>
    /// <param name="settings">ApplicationsSetting für Height and Width</param>
    /// <param name="pathHeigthSetting">Name für Height Setting</param>
    /// <param name="pathWidthSetting">Name für Width Setting</param>
    /// <param name="minHeigth">Minimum Height</param>
    /// <param name="minWidth">Minimum Width</param>
    /// <param name="maxHeigth">Maximum Height</param>
    /// <param name="maxWidth">Maximum Width</param>
    /// <returns>true wenn DialogResult=true, ansonsten false</returns>
    public bool? ShowDialog(string titel, object datacontext, ApplicationSettingsBase settings, string pathHeigthSetting, string pathWidthSetting, double minHeigth = 0, double minWidth = 0, double maxHeigth = double.PositiveInfinity, double maxWidth = double.PositiveInfinity)
    {
        var win = new DialogWindow { Title = titel, DataContext = datacontext };
 
        win.Owner = Application.Current.MainWindow;
        win.ShowInTaskbar = false;
        win.MinHeight = minHeigth;
        win.MinWidth = minWidth;
        win.MaxHeight = maxHeigth;
        win.MaxWidth = maxWidth;
 
        try
        {
            if(settings != null)
            {
                win.SizeToContent = SizeToContent.Manual;
 
                var height = settings[pathHeigthSetting];
                var width = settings[pathWidthSetting];
 
                BindingOperations.SetBinding(win, FrameworkElement.HeightProperty, new Binding(pathHeigthSetting) { Source = settings, Mode = BindingMode.TwoWay });
                BindingOperations.SetBinding(win, FrameworkElement.WidthProperty, new Binding(pathWidthSetting) { Source = settings, Mode = BindingMode.TwoWay });
 
                win.Height = (double)height;
                win.Width = (double)width;
            }
        }
        catch
        {
            win.SizeToContent = SizeToContent.WidthAndHeight;
        }
        return win.ShowDialog();
    }
        #endregion
}


Eine Interface um ein DialogResult=true zurück zugeben.

public class RequestCloseDialogEventArgs : EventArgs
{
    public RequestCloseDialogEventArgs(bool dialogresult)
    {
        this.DialogResult = dialogresult;
    }
 
    public bool DialogResult
    {
        getset;
    }
}
 
public interface IDialogResultVMHelper
{
    event EventHandler<RequestCloseDialogEventArgs> RequestCloseDialog;
}


Und damit auch irgendwas angezeigt wird, ein einfaches Fenster zum Anzeigen der Dialoge.

<Window x:Class="DialogWindow"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300"
        WindowStyle="SingleBorderWindow" 
        WindowStartupLocation="CenterOwner" SizeToContent="WidthAndHeight">
    <ContentPresenter x:Name="DialogPresenter" Content="{Binding .}">
 
    </ContentPresenter>
</Window>
public partial class DialogWindow : Window
{
    //Merken wenn Window geschlossen wurde, damit kein DialogResult mehr gesetzt wird
    private bool _isClosed = false;
 
    public DialogWindow()
    {
        InitializeComponent();
        this.DialogPresenter.DataContextChanged += DialogPresenterDataContextChanged;
        this.Closed += DialogWindowClosed;
    }
 
    void DialogWindowClosed(object sender, EventArgs e)
    {
        this._isClosed = true;
    }
 
    private void DialogPresenterDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        var d = e.NewValue as IDialogResultVMHelper;
 
        if (d == null)
            return;
 
        d.RequestCloseDialog += new EventHandler<RequestCloseDialogEventArgs>(DialogResultTrueEvent).MakeWeak(eh => d.RequestCloseDialog -= eh); ;
    }
 
    private void DialogResultTrueEvent(object sender, RequestCloseDialogEventArgs eventargs)
    {
        //Wichtig damit für ein geschlossenes Window kein DialogResult mehr gesetzt wird
        //GC räumt Window irgendwann weg und durch MakeWeak fliegt es auch beim IDialogResultVMHelper raus
        if(_isClosed) return;
 
        this.DialogResult = eventargs.DialogResult;
    }
}


Damit wäre man jetzt im Prinzip fertig. Jetzt nutzt man einfach die Vorteile von DataTemplates im WPF um ein Viewmodel anzuzeigen. D.h. es muss ein DataTemplate für die entsprechenden DialogViewmodels in den Resourcen (z.b. app.xaml) anlegen.

<DataTemplate DataType="{x:Type viewmodels:TestDialogViewmodel}">
        <views:TestDialogView />
</DataTemplate>

Dienstag, 20. Januar 2015

WPF - TextBox Input Behavior

Hier mal ein nettes Behavior was ich recht häufig benutze, um Eingaben in einer TextBox zu beschränken. Die Anwendung ist ganz einfach:

<TextBox>
    <i:Interaction.Behaviors>
        <Behaviors:TextBoxInputBehavior InputMode="DigitInput" />
    </i:Interaction.Behaviors>
</TextBox>    


Und hier der Code dazu:

public class TextBoxInputBehavior : Behavior<TextBox>
{
    const NumberStyles validNumberStyles = NumberStyles.AllowDecimalPoint |
                                           NumberStyles.AllowThousands |
                                           NumberStyles.AllowLeadingSign;
    public TextBoxInputBehavior()
    {
        this.InputMode = TextBoxInputMode.None;
        this.JustPositivDecimalInput = false;
        this.MaxVorkommastellen = null;
    }
 
    public TextBoxInputMode InputMode { getset; }
 
    public ushort? MaxVorkommastellen { getset; }
    
    public static readonly DependencyProperty JustPositivDecimalInputProperty =
     DependencyProperty.Register("JustPositivDecimalInput"typeof(bool),
     typeof(TextBoxInputBehavior), new FrameworkPropertyMetadata(false));
 
    public bool JustPositivDecimalInput
    {
        get { return (bool)GetValue(JustPositivDecimalInputProperty); }
        set { SetValue(JustPositivDecimalInputProperty, value); }
    }
 
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.PreviewTextInput += AssociatedObjectPreviewTextInput;
        AssociatedObject.PreviewKeyDown += AssociatedObjectPreviewKeyDown;
 
        DataObject.AddPastingHandler(AssociatedObject, Pasting);
 
    }
 
    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.PreviewTextInput -= AssociatedObjectPreviewTextInput;
        AssociatedObject.PreviewKeyDown -= AssociatedObjectPreviewKeyDown;
 
        DataObject.RemovePastingHandler(AssociatedObject, Pasting);
    }
 
    private void Pasting(object sender, DataObjectPastingEventArgs e)
    {
        if (e.DataObject.GetDataPresent(typeof(string)))
        {
            var pastedText = (string)e.DataObject.GetData(typeof(string));
 
            if (!this.IsValidInput(this.GetText(pastedText)))
            {
                System.Media.SystemSounds.Beep.Play();
                e.CancelCommand();
            }
        }
        else
        {
            System.Media.SystemSounds.Beep.Play();
            e.CancelCommand();
        }
    }
 
    private void AssociatedObjectPreviewKeyDown(object sender, KeyEventArgs e)
    {
        if (e.Key == Key.Space)
        {
            if (!this.IsValidInput(this.GetText(" ")))
            {
                System.Media.SystemSounds.Beep.Play();
                e.Handled = true;
            }
        }
 
        if (e.Key == Key.Back)
        {
            //wenn was selektiert wird dann wird nur das gelöscht mit BACK
            if (this.AssociatedObject.SelectionLength > 0)
            {
                if (!this.IsValidInput(this.GetText("")))
                {
                    System.Media.SystemSounds.Beep.Play();
                    e.Handled = true;
                }
            }
            else if(this.AssociatedObject.CaretIndex > 0)
            {
                //selber löschen
                var txt = this.AssociatedObject.Text;
                var backspace = txt.Remove(this.AssociatedObject.CaretIndex - 1, 1);
 
                if (!this.IsValidInput(backspace))
                {
                    System.Media.SystemSounds.Beep.Play();
                    e.Handled = true;
                }
            }
        }
 
        if (e.Key == Key.Delete)
        {
            //wenn was selektiert wird dann wird nur das gelöscht mit ENTF
            if (this.AssociatedObject.SelectionLength > 0)
            {
                if (!this.IsValidInput(this.GetText("")))
                {
                    System.Media.SystemSounds.Beep.Play();
                    e.Handled = true;
                }
            }
            else if (this.AssociatedObject.CaretIndex < this.AssociatedObject.Text.Length)
            {
                //selber löschen
                var txt = this.AssociatedObject.Text;
                var entf = txt.Remove(this.AssociatedObject.CaretIndex, 1);
 
                if (!this.IsValidInput(entf))
                {
                    System.Media.SystemSounds.Beep.Play();
                    e.Handled = true;
                }
            }
        }
    }
 
    private void AssociatedObjectPreviewTextInput(object sender, TextCompositionEventArgs e)
    {
        if (!this.IsValidInput(this.GetText(e.Text)))
        {
            System.Media.SystemSounds.Beep.Play();
            e.Handled = true;
        }
    }
 
    private string GetText(string input)
    {
        var txt = this.AssociatedObject;
 
        int selectionStart = txt.SelectionStart;
        if (txt.Text.Length < selectionStart) 
            selectionStart = txt.Text.Length;
 
        int selectionLength = txt.SelectionLength;
        if (txt.Text.Length < selectionStart + selectionLength) 
            selectionLength = txt.Text.Length - selectionStart;
 
        var realtext = txt.Text.Remove(selectionStart, selectionLength);
 
        int caretIndex = txt.CaretIndex;
        if (realtext.Length < caretIndex) 
            caretIndex = realtext.Length;
 
        var newtext = realtext.Insert(caretIndex, input);
 
        return newtext;
    }
 
    private bool IsValidInput(string input)
    {
        if (input.Length == 0)
            return true;
 
        switch (InputMode)
        {
            case TextBoxInputMode.None:
                return true;
            case TextBoxInputMode.DigitInput:
                return CheckIsDigit(input);
 
            case TextBoxInputMode.DecimalInput:
                decimal d;
                //wen mehr als ein Komma
                if (input.ToCharArray().Where(x => x == ',').Count() > 1)
                    return false;
 
                if (input.Contains("-"))
                {
                    if (this.JustPositivDecimalInput) 
                        return false;
 
                    
                    if (input.IndexOf("-",StringComparison.Ordinal) > 0) 
                        return false;
 
                    if(input.ToCharArray().Count(x=>x=='-') > 1)
                        return false;
 
                    //minus einmal am anfang zulässig
                    if (input.Length == 1) 
                        return true;
                }
               
                var result = decimal.TryParse(input, validNumberStyles, CultureInfo.CurrentCulture, out d);
                return result;
                
            case TextBoxInputMode.PercentInput: //99,999 is zulässig und  nur positiv ohne 1000er Trennzeichen
                float f;
 
                if (input.Contains("-"))
                    return false;
                //wen mehr als ein Komma
                if (input.ToCharArray().Where(x => x == ',').Count() > 1)
                    return false;
 
                var percentResult = float.TryParse(input, NumberStyles.Float, CultureInfo.CurrentCulture, out f);
 
                if (MaxVorkommastellen.HasValue)
                {
                    var vorkomma = Math.Truncate(f);
                    if (vorkomma.ToString(CultureInfo.CurrentCulture).Length > MaxVorkommastellen.Value)
                        return false;
                }
 
                return percentResult;
                
            defaultthrow new ArgumentException("Unknown TextBoxInputMode");
 
        }
        return true;
    }
 
    private bool CheckIsDigit(string wert)
    {
        return wert.ToCharArray().All(Char.IsDigit);
    }
}
 
public enum TextBoxInputMode
{
    None,
    DecimalInput,
    DigitInput,
    PercentInput
}