In WPF the Password property of a PasswordBox is not a DependencyProperty so I can't bind to it directly. As a workaround I am using this PasswordHelper from https://www.wpftutorial.net/PasswordBox.html which attaches a PasswordHelper.Password to the PasswordBox so I can bind to it.
To prevent the password from staying in the DataContext as plain text I would like to use a converter which generates a salted hash of the password before saving it to the DataContext. As I need to save both the salt and the salted hash to the DataContext I am using a MultiBinding and a IMultiValueConverter converter StringToSaltedHashConverter.
My problem is that the Password and PasswordSalt properties of my DataContext are not getting updated when I fill the PasswordBox. I have checked with Snoop and both the Password and PasswordHelper.Password properties of the PasswordBoxare changing accordingly to what I type. Also my StringToSaltedHashConverter is being called and I am returning the correct values.
I have some other bindings on this form (username, first name, last name, gender...) and they all work fine. This is the only one that is not updating.
¿Why is my DataContext not getting updated?
XAML:
<PasswordBox x:Name="Password"
Style="{DynamicResource PasswordBoxStyle1}"
local:PasswordHelper.Attach="True">
<local:PasswordHelper.Password>
<MultiBinding Converter="{StaticResource StringToSaltedHashConverter}"
Mode="OneWayToSource">
<Binding Path="Password" />
<Binding Path="PasswordSalt" />
</MultiBinding>
</local:PasswordHelper.Password>
</PasswordBox>
Code behind:
public class StringToSaltedHashConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
string str = value as string;
string salt = Hash.CreateSalt();
string hash = Hash.CreateHash(str, salt);
object[] vs = { hash, salt };
return vs;
}
}
This is not the way to go. PasswordHelper class and their likes should be banned from the internet. The PasswordBox.Password property is not bindable for a very good reason which is well documented. Accessing this property will create a plain text string representation which can be effordlessly retrieved with free tools e.g. Microsoft Process Explorer.
When you get the Password property value, you expose the password as plain text in memory. To avoid this potential security risk, use the SecurePassword property to get the password as a SecureString.
Setting this property to null causes the underlying password to be set to Empty.
You even store the plain salt value in memory - I really hope this is neither a commercial nor a public application. You have absolutely no control when the Garbage Collector will remove any value from the memory. Since String is an immutable type, it is very likely that multiple copies of the user's password will remain public in memory.
Recommended authentication in a Windows environment is to use the Windows User Authentication API.
You should at least clear the PasswordBox.Password property, by setting it null.
Encryption should not be done in the view.
And please, write responsible code! User data is not your data!
MainWindow.xaml
<Window>
<Window.DataContext>
<ViewModel />
</Window.DataContext>
<PasswordBox x:Name="PasswordBox" />
</Window>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.PasswordBox.PasswordChanged += OnPasswordChanged;
}
private void OnPasswordChanged(object sender, RoutedEventArgs e)
{
(this.DataContext as ViewModel).Password = this.PasswordBox.SecurePassword;
}
}
ViewModel.cs
pucblic class ViewModel
{
private Model Model { get; }
private SecureString password;
public SecureString Password
{
private get => this.password;
public set
{
this.password = value;
OnPasswordChanged();
}
}
private void OnPasswordChanged()
{
// Hash password in the model e.g. to compare it to a hashed database value
this.Model.TryLogin(this.Password);
}
}
Model.cs
public class Model
{
public bool TryLogin(SecureString password)
{
IntPtr unmanagedString = Marshal.SecureStringToGlobalAllocUnicode(password);
string hashedPassword = HashPassword(Marshal.PtrToStringUni(unmanagedString));
return PasswordIsValid(hashedPassword);
}
private string HashPassword(string unsecurePassword)
{
// Hash algorithm
}
}
Before I need the password from a PasswordBox the User will tell me that he has finished with input by pressing a button.
The best option for me is to use the CommandParameter of the Button with the LoginCommand:
<StackPanel
Width="300"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Label Content="Username:" />
<TextBox Text="{Binding Username}" />
<Label Content="Password:" />
<PasswordBox PasswordChanged="PasswordChanged" />
<StackPanel
Orientation="Horizontal">
<Button
Name="LoginButton"
Command="{Binding LoginCommand}"
Content="Login" />
</StackPanel>
</StackPanel>
View code
private void PasswordChanged( object sender, RoutedEventArgs e )
{
LoginButton.CommandParameter = ( sender as PasswordBox ).SecurePassword;
}
ViewModel Command
Login = ReactiveCommand.CreateFromTask( async ( SecureString password, CancellationToken cancellationToken ) =>
{
var loggedIn = await AuthenticationService.LoginAsync( Username, password, cancellationToken );
...
} );
I just wanted to thank everyone for your answers and insighful comments. You made me realize that I should not be using a PasswordHelper and that I should not be trying to bind to the password at all.
Just in case anyone is having a similar problem with a MultiBinding not properly updating the DataContext, this can be fixed by adding the OneWayToSource mode to each of the bindings inside the MultiBinding.
<MultiBinding Converter="{StaticResource StringToSaltedHashConverter}"
Mode="OneWayToSource">
<Binding Path="Password"
Mode="OneWayToSource" />
<Binding Path="PasswordSalt"
Mode="OneWayToSource" />
</MultiBinding>
Related
Binding to double may produce following validation error
Value ... could not be converted.
When using ExceptionValidationRule errors are more talkative:
Input string was not in a correct format.
Value was either too big or too small for a ...
Perhaps there are more. Add to them those what I can throw myself in bound property setter.
Now, I want to localize those messages (preferably second version). Not a big surprise, but sources doesn't reveal anything useful (am I looking at wrong place?).
I can make my own validation rules, but perhaps there is an easier way?
My question: can I localize ExceptionValidationRule?
Below is mcve:
xaml:
<TextBox>
<TextBox.Text>
<Binding Path="TestDouble" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<!--<local:MyValidationRule />-->
<!--<ExceptionValidationRule />-->
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
<Validation.ErrorTemplate>
<ControlTemplate>
<TextBlock Margin="0,20,0,0" Foreground="Red" Text="{Binding ErrorContent}" />
</ControlTemplate>
</Validation.ErrorTemplate>
</TextBox>
cs:
public partial class MainWindow : Window
{
public double TestDouble { get; set; }
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
}
public class MyValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo) => ValidationResult.ValidResult; // not used
public override ValidationResult Validate(object value, CultureInfo cultureInfo, BindingExpressionBase owner)
{
var bindingExpression = owner as BindingExpression;
if (bindingExpression != null)
{
var type = bindingExpression.ResolvedSource.GetType().GetProperty(bindingExpression.ResolvedSourcePropertyName).PropertyType;
if (type == typeof(double))
{
double result;
if (!double.TryParse((string)value, out result))
return new ValidationResult(false, "The value format is not recognized"); // custom message
}
... // and so on, for all types ?????
}
return base.Validate(value, cultureInfo, owner);
}
}
To localize such error messages, which appears during updating source of binding, its enough to setup UpdateSourceExceptionFilter callback handler of binding, no custom validation rule is needed (ExceptionValidationRule is required).
xaml:
<Binding UpdateSourceExceptionFilter="ExeptionFilter" ...>
cs:
object ExeptionFilter(object bindingExpression, Exception exception)
{
return "Localized error message";
}
Obviously it can be switch/case (in C# 7, earlier - if/else if) to provide exception-specific messages (in my question those are FormatException and OverflowException correspondingly).
How do I enable (and conversely disable) a button when a textbox has a value, and a combo box has a selected item?
How could I set up the bindings to get the button to disable/enable appropriately?
This is not the way you should think. WPF encourages to use MVVM so you should prepare your VM class so that it has the appropriate properties that you should bind to(and probably model class too). Do not put logic/validation logic into your GUI.
Why not consider to use command binding? See/try for example the following simplified example:
<Window.CommandBindings>
<CommandBinding Command="Save" CanExecute="CommandBinding_CanExecute" Executed="CommandBinding_Executed" />
</Window.CommandBindings>
<StackPanel>
<TextBox Name="TextBox1"/>
<Button Content="Save" Command="Save"/>
</StackPanel>
CommandBinding has a property [CanExecute] that you can use for enabling/disabling your button in the code behind:
private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = (this.TextBox1.Text == "test");
}
private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
{
// put your command-logic here
}
In this example you have to enter the value "test" to enable the button and execute your command-logic.
Bind the Button to a Command (e.g. Save-Command)
Bind the TextBox.Text to a property (e.g. string MyTextBoxText)
Bind the SelectedItem of the ComboBox to a property (or even the itemSource) (e.g. object MySelectedItem)
The CanExecute of the command has code like this:
return !string.IsNullOrWhiteSpace(MyTextBoxText) && (MySelectedItem != null);
Another way to do this is using a MultiBinding and Converter on the button you want to enable/disable
<Window ... xmlns:local="...">
<Window.Resources>
<local:MyMultiValueConverter x:Key="MyMultiValueConverter" />
</Window.Resources>
...
<ComboBox x:Name="myComboBox">...</ComboBox>
<TextBox x:Name="myTextBox">...</TextBox>
...
<Button Content="My Button">
<Button.IsEnabled>
<MultiBinding Converter="{StaticResource MyMultiValueConverter}">
<Binding ElementName="myComboBox" Path="SelectedValue" />
<Binding ElementName="myTextBox" Path="Text" />
</MultiBinding>
</Button.IsEnabled>
</Button>
...
</Window>
You need to create an implementation of the IMultiValueConverter interface that tests the values of both the ComboBox.SelectedValue and TextBox.Text properties and return either true or false which will then be assigned to the Button.IsEnabled property. Here's a simple converter that works but you'd want to make sure to tailor one to your specific needs:
public class MyMultiValueConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (values == null)
return false;
return values.All(c => c is String ? !String.IsNullOrEmpty((string)c) : c != null);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
While this approach does work, I tend to agree with the other answers in that you should use commands when possible over multibindings and converters.
What's wrong with these code, the Validation.Error is never fired whereas I setthe and the NotifyOnValidationError property to True. So, the method "Grid_Error(object sender, ValidationErrorEventArgs e)" is never executed, but I don't know why :(
<Window xmlns:my="clr-namespace:WpfDigitalClock;assembly=WpfDigitalClock" x:Class="WpfApplication1.MainWindow"
xmlns:local="clr-namespace:WpfApplication1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<local:Names x:Key="MyNames" />
</Window.Resources>
<Grid Validation.Error="Grid_Error">
<TextBox Height="21" Margin="12,62,0,0" Name="TextBox1" VerticalAlignment="Top" HorizontalAlignment="Left" Width="120">
<TextBox.Text>
<Binding Source="{StaticResource MyNames}" Path="FirstName" NotifyOnValidationError="True">
<Binding.ValidationRules>
<local:StringValidator />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<TextBox Height="21" HorizontalAlignment="Right" Margin="0,62,12,0" Name="TextBox2" VerticalAlignment="Top" Width="120" >
<TextBox.Text>
<Binding Source="{StaticResource MyNames}" Path="LastName" NotifyOnValidationError="True">
<Binding.ValidationRules>
<local:StringValidator />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Button HorizontalAlignment="Left" Margin="35,122,0,116" Name="Button1" Width="75" Click="Button1_Click">Back</Button>
<Button HorizontalAlignment="Right" Margin="0,122,34,117" Name="Button2" Width="75" Click="Button2_Click">Forward</Button>
<Button Height="22" Margin="101,0,101,56" Name="Button3" VerticalAlignment="Bottom" Click="Button3_Click">Add</Button>
</Grid>
in the Window1.xaml.cs file :
public class StringValidator : ValidationRule
{
public override ValidationResult Validate(object value,
System.Globalization.CultureInfo cultureinfo)
{
string aString = value.ToString();
if (aString == "")
return new ValidationResult(false, "String cannot be null");
return new ValidationResult(true, null);
}
}
private void Grid_Error(object sender, ValidationErrorEventArgs e)
{
if(e.Action == ValidationErrorEventAction.Added)
MessageBox.Show(e.Error.ErrorContent.ToString());
}
Thank you for your help !
EDIT :
Here my Names Class :
class Names : ObservableCollection<Name>
{
public Names ()
{
Name aName = new Name("FirstName " + (this.Count +1).ToString(),
"LastName " + (this.Count + 1).ToString());
this.Add(aName);
}
}
Here my Name class :
class Name : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _firstName;
private string _lastName;
public Name(string fName, string lName)
{
_firstName = fName;
_lastName = lName;
}
public string FirstName
{
get
{
return _firstName;
}
set
{
_firstName = value;
PropertyChanged(this, new PropertyChangedEventArgs("FirstName"));
}
}
public string LastName
{
get
{
return _lastName;
}
set
{
_lastName = value;
PropertyChanged(this, new PropertyChangedEventArgs("LastName"));
}
}
}
The application cannot modify the content of this collection. See the Example section for an example of how to use this attached property.
The WPF data binding model enables you to associate ValidationRules with your Binding object. Validation occurs during binding target-to-binding source value transfer before the converter is called. The following describes the validation process:
1.When a value is being transferred from the target property to the source property, the data binding engine first removes any ValidationError that may have been added to the Validation.Errors attached property of the bound element. It then checks if there are any custom ValidationRules defined for that Binding, in which case it calls the Validate method on each of the ValidationRules until one of them runs into an error or until all of them pass.
2.Once there is a custom rule that does not pass, the binding engine creates a ValidationError object and adds it to the Validation.Errors collection of the bound element. When Validation.Errors is not empty, the Validation.HasError attached property of the element is set to true. Also, if the NotifyOnValidationError property of the Binding is set to true, then the binding engine raises the Validation.Error attached event on the element.
3.If all of the rules pass, the binding engine then calls the converter, if one exists.
4.If the converter passes, the binding engine calls the setter of the source property.
5.If the binding has an ExceptionValidationRule associated with it and an exception is thrown during step
4, the binding engine checks to see if there is a UpdateSourceExceptionFilter. You have the option to use the UpdateSourceExceptionFilter callback to provide a custom handler for handling exceptions. If an UpdateSourceExceptionFilter is not specified on the Binding, the binding engine creates a ValidationError with the exception and adds it to the Validation.Errors collection of the bound element.
Also note that a valid value transfer in either direction (target-to-source or source-to-target) clears the Validation.Errors attached property.
For information about the behavior of this property in MultiBinding scenarios, see ValidationError.
From your comment i would conclude that the ValidationRule does not return an error, hence the error event is not fired. Try stepping through the Validation-method with the debugger.
Also, validation is only performed upon a source-update, in TextBoxes that normally happens on LostFocus.
Edit: MyNames is a collection, it not have the properties you try to bind to, there should be binding errors in the Output window.
If you want to bind to the first element you need to change the path to something like [0].LastName for the last-name-binding.
Does your Names class implement INotifyPropertyChanged?
In the code-behind file of the file, set your datacontext to this. Expose your Names object as a property there and see if that works. I'm not comfortable with binding to the static resource in the window.
I have data comming back from web service in the form of a ObservableCollection<string> I want to bind the collection to a read-only TextBox so that the user can select and copy the data to the clipboard.
To get the collection bound to the Text property of the TextBox I created IValueConverter which converts the collection to a text string. This seems to work except that it only works once, it is as if the binding does not recognize subsequent changes to the Observable collection. Here is a simple application that reproduces the problem, just to confirm the binding is working correctly I also bind to a `ListBox'
Is this because the Text binding simple does not handle the change events of the collection?
One option would of course be for me to handle the collection changes and propogate those to a Text property that the TextBox is bound to, which is fine, but I would like to understand why what seemed to me to be an obvious solutions is not working as expected.
XAML
<Window x:Class="WpfTextBoxBinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfTextBoxBinding"
Title="MainWindow" Height="331" Width="402">
<StackPanel>
<StackPanel.Resources>
<local:EnumarableToTextConverter x:Key="EnumarableToTextConverter" />
</StackPanel.Resources>
<TextBox Text="{Binding TextLines, Mode=OneWay, Converter={StaticResource EnumarableToTextConverter}}" Height="100" />
<ListBox ItemsSource="{Binding TextLines}" Height="100" />
<Button Click="Button_Click" Content="Add Line" />
</StackPanel >
</Window>
Code Behind
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.Windows;
using System.Windows.Data;
using System.Globalization;
namespace WpfTextBoxBinding
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public ObservableCollection<string> TextLines {get;set;}
public MainWindow()
{
DataContext = this;
TextLines = new ObservableCollection<string>();
// Add some initial data, this shows that the
// TextBox binding works the first time
TextLines.Add("First Line");
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
TextLines.Add("Line :" + TextLines.Count);
}
}
public class EnumarableToTextConverter : IValueConverter
{
public object Convert(
object value, Type targetType,
object parameter, CultureInfo culture)
{
if (value is IEnumerable)
{
StringBuilder sb = new StringBuilder();
foreach (var s in value as IEnumerable)
{
sb.AppendLine(s.ToString());
}
return sb.ToString();
}
return string.Empty;
}
public object ConvertBack(
object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
A slightly more elegant way to achieve that is to use MultiBinding on the Text property and bind to the Collection's Count property. This will update the binding every time the collection's Count changes and update the Text according to a MultiValueConverter you define.
<TextBox>
<TextBox.Text>
<MultiBinding Converter="{x:Static l:Converters.LogEntryCollectionToTextConverter}">
<Binding Path="LogEntries" Mode="OneWay"/>
<Binding Path="LogEntries.Count" Mode="OneWay" />
</MultiBinding>
</TextBox.Text>
</TextBox>
And the converter:
public static class Converters
{
public static LogEntryCollectionToTextConverter LogEntryCollectionToTextConverter = new LogEntryCollectionToTextConverter();
}
public class LogEntryCollectionToTextConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
ObservableCollection<LogEntry> logEntries = values[0] as ObservableCollection<LogEntry>;
if (logEntries != null && logEntries.Count > 0)
return logEntries.ToString();
else
return String.Empty;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
In my use case, I don't allow the TextBox to update its source (hence the ´Mode="OneWay"´), but if need be the Converter's ConvertBack method would handle that.
Is this because the Text binding
simple does not handle the change
events of the collection?
Indeed. A binding updates only when its source property changes. If you change the TextLines property by setting a whole new ObservableCollection and implement INotifyPropertyChanged, your binding will work as expected. Adding new elements to the collection will have meaning only if it's bound to a property like ItemsControl.ItemsSource that listens to the collection changes.
One option would of course be for me
to handle the collection changes and
propogate those to a Text property
that the TextBox is bound to, which is
fine.
That would be another solution.
update below code
private void Button_Click(object sender, RoutedEventArgs e)
{
TextLines.Add("Line :" + TextLines.Count);
BindingExpression be = BindingOperations.GetBindingExpression(txtName, TextBox.TextProperty);
be.UpdateTarget();
}
where txtName is your name of your textbox
MVVM way
1- Difine a property of type string in your ViewModel as shown below and bind this property to the textbox text property a shown below and remove ValueConverter no need now.
public string TextLines {get;set;}
<TextBox Text="{Binding TextLines, Mode=OneWay/>
2- I think , you most probably handling button click event using a Command Handler say your Command is AddMoreLines
so in the AddMoreLine Command Handler , after adding a new object in your OBservrableCollection , create a StringBuilder and append all the content of your Collection and assign the string to the property created in step 1.
3- Call PropertyChanged Handler.
I have a command which I am executing from my XAML file using the following standard syntax:
<Button Content="Zoom" Command="{Binding MyViewModel.ZoomCommand}"/>
This worked fine until I realized that I needed TWO pieces of information from the view in order to make this operation complete the way users expect (the width and height of the canvas specfically).
It seems like it's possible to pass an array as an argument to my command, but I don't see there being a way to specify the binding to my two canvas properties in the CommandParameter:
<Button Content="Zoom"
Command="{Binding MyViewModel.ZoomCommand"
CommandParameter="{Binding ElementName=MyCanvas, Path=Width}"/>
How do I pass both Width and Height to my command? It doesn't seem like this is possible using commands from XAML and I need to wire up a click handler in my codebehind to get this information to pass to my zoom method.
Firstly, if you're doing MVVM you would typically have this information available to your VM via separate properties bound from the view. That saves you having to pass any parameters at all to your commands.
However, you could also multi-bind and use a converter to create the parameters:
<Button Content="Zoom" Command="{Binding MyViewModel.ZoomCommand">
<Button.CommandParameter>
<MultiBinding Converter="{StaticResource YourConverter}">
<Binding Path="Width" ElementName="MyCanvas"/>
<Binding Path="Height" ElementName="MyCanvas"/>
</MultiBinding>
</Button.CommandParameter>
</Button>
In your converter:
public class YourConverter : IMultiValueConverter
{
public object Convert(object[] values, ...)
{
return values.Clone();
}
...
}
Then, in your command execution logic:
public void OnExecute(object parameter)
{
var values = (object[])parameter;
var width = (double)values[0];
var height = (double)values[1];
}
In the converter of the chosen solution, you should add values.Clone() otherwise the parameters in the command end null
public class YourConverter : IMultiValueConverter
{
public object Convert(object[] values, ...)
{
return values.Clone();
}
...
}
Use Tuple in Converter, and in OnExecute, cast the parameter object back to Tuple.
public class YourConverter : IMultiValueConverter
{
public object Convert(object[] values, ...)
{
Tuple<string, string> tuple = new Tuple<string, string>(
(string)values[0], (string)values[1]);
return (object)tuple;
}
}
// ...
public void OnExecute(object parameter)
{
var param = (Tuple<string, string>) parameter;
}
If your values are static, you can use x:Array:
<Button Command="{Binding MyCommand}">10
<Button.CommandParameter>
<x:Array Type="system:Object">
<system:String>Y</system:String>
<system:Double>10</system:Double>
</x:Array>
</Button.CommandParameter>
</Button>
About using Tuple in Converter, it would be better to use 'object' instead of 'string', so that it works for all types of objects without limitation of 'string' object.
public class YourConverter : IMultiValueConverter
{
public object Convert(object[] values, ...)
{
Tuple<object, object> tuple = new Tuple<object, object>(values[0], values[1]);
return tuple;
}
}
Then execution logic in Command could be like this
public void OnExecute(object parameter)
{
var param = (Tuple<object, object>) parameter;
// e.g. for two TextBox object
var txtZip = (System.Windows.Controls.TextBox)param.Item1;
var txtCity = (System.Windows.Controls.TextBox)param.Item2;
}
and multi-bind with converter to create the parameters (with two TextBox objects)
<Button Content="Zip/City paste" Command="{Binding PasteClick}" >
<Button.CommandParameter>
<MultiBinding Converter="{StaticResource YourConvert}">
<Binding ElementName="txtZip"/>
<Binding ElementName="txtCity"/>
</MultiBinding>
</Button.CommandParameter>
</Button>
This task can also be solved with a different approach. Instead of programming a converter and enlarging the code in the XAML, you can also aggregate the various parameters in the ViewModel. As a result, the ViewModel then has one more property that contains all parameters.
An example of my current application, which also let me deal with the topic.
A generic RelayCommand is required: https://stackoverflow.com/a/22286816/7678085
The ViewModelBase is extended here by a command SaveAndClose. The generic type is a named tuple that represents the various parameters.
public ICommand SaveAndCloseCommand => saveAndCloseCommand ??= new RelayCommand<(IBaseModel Item, Window Window)>
(execute =>
{
execute.Item.Save();
execute.Window?.Close(); // if NULL it isn't closed.
},
canExecute =>
{
return canExecute.Item?.IsItemValide ?? false;
});
private ICommand saveAndCloseCommand;
Then it contains a property according to the generic type:
public (IBaseModel Item, Window Window) SaveAndCloseParameter
{
get => saveAndCloseParameter ;
set
{
SetProperty(ref saveAndCloseParameter, value);
}
}
private (IBaseModel Item, Window Window) saveAndCloseParameter;
The XAML code of the view then looks like this:
(Pay attention to the classic click event)
<Button
Command="{Binding SaveAndCloseCommand}"
CommandParameter="{Binding SaveAndCloseParameter}"
Click="ButtonApply_Click"
Content="Apply"
Height="25" Width="100" />
<Button
Command="{Binding SaveAndCloseCommand}"
CommandParameter="{Binding SaveAndCloseParameter}"
Click="ButtonSave_Click"
Content="Save"
Height="25" Width="100" />
and in the code behind of the view, then evaluating the click events, which then set the parameter property.
private void ButtonApply_Click(object sender, RoutedEventArgs e)
{
computerViewModel.SaveAndCloseParameter = (computerViewModel.Computer, null);
}
private void ButtonSave_Click(object sender, RoutedEventArgs e)
{
computerViewModel.SaveAndCloseParameter = (computerViewModel.Computer, this);
}
Personally, I think that using the click events is not a break with the MVVM pattern. The program flow control is still located in the area of the ViewModel.