Correct usage of delegated commands using Prism-MVVM - c#

I'm trying to get the mouse position related to a wpf control (a Canvas in this case) using MVVM Framework with Prism Library.
I already got a solution but I'm not sure if it's a correct way to use the MVVM framework.
Main window:
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="250"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" BorderBrush="Gray" BorderThickness="1">
<Canvas HorizontalAlignment="Center" VerticalAlignment="Center"
Width="{Binding CanvasWidth}" Height="{Binding CanvasHeight}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseMove">
<prism:InvokeCommandAction Command="{Binding MouseMove}"/>
</i:EventTrigger>
<i:EventTrigger EventName="Loaded">
<prism:InvokeCommandAction Command="{Binding Loaded}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<Image Source="{Binding Image}" />
</Canvas>
</Border>
<TextBlock Grid.Column="1" Text="{Binding Text}"/>
<StackPanel Grid.Column="1">
<TextBlock Text="{Binding MouseX, StringFormat='X={0}'}" Grid.Column="1" />
<TextBlock Text="{Binding MouseY, StringFormat='Y={0}'}" Grid.Column="1" />
</StackPanel>
</Grid>
In this XAML code snippet the canvas has 2 Event triggers that I use for converting:
the "MouseMove" event to give the XY pointer position
and the "Loaded" event where the tricky part is. Here I pass the instance obj from Canvas to the controller through this EventTrigger, the in the controller I use this code:
Loaded and MouseMove commands definition:
public DelegateCommand<MouseEventArgs> MouseMove { get; private set; }
public DelegateCommand<RoutedEventArgs> Loaded { get; private set; }
Constructor:
public MainWindowViewModel()
{
MouseMove = new DelegateCommand<MouseEventArgs>(GetMousePosition);
Loaded = new DelegateCommand<RoutedEventArgs>(GetCanvas);
}
Properties definition:
private string _mouseX;
public string MouseX
{
get { return _mouseX; }
set { SetProperty(ref _mouseX, value); }
}
private string _mouseY;
public string MouseY
{
get { return _mouseY; }
set { SetProperty(ref _mouseY, value); }
}
private System.Windows.Controls.Canvas _canvas;
public System.Windows.Controls.Canvas Canvas
{
get { return _canvas; }
set { SetProperty(ref _canvas, value); }
}
Methods called by commands:
private void GetCanvas(RoutedEventArgs obj)
{
Canvas = (System.Windows.Controls.Canvas)obj.Source;
}
private void GetMousePosition(MouseEventArgs eventParam)
{
Point position = eventParam.GetPosition(Canvas);
MouseX = position.X.ToString();
MouseY = position.Y.ToString();
}
Is this way a correct usage? Even this working I feel like passing the Canvas obj to the controller I'm doing something like "code behind".

I'm using a converter to do the GetPosition. That gets passed the source and the event args, so you can get away without the LoadedCommand and you keep the MouseEventArgs out of your view model.
xaml:
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseMove">
<i:InvokeCommandAction Command="{Binding MouseMoveCommand}" PassEventArgsToCommand="True" EventArgsConverter="{StaticResource GetPositionConverter}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
view model:
public DelegateCommand<Point?> MouseMoveCommand { get; }
converter:
internal class GetPositionConverter : IValueConverter
{
public object Convert( object value, Type targetType, object parameter, CultureInfo culture )
{
var mouseEventArgs = (MouseEventArgs)value;
return mouseEventArgs.GetPosition( (IInputElement)mouseEventArgs.Source );
}
public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture )
{
throw new NotImplementedException();
}
}
The converter should have at least minimal error handling, though, this is just an example :-)

I think you are violating MVVM.. because you are referencing UI-type (i.e. System.Windows.Controls.Canvas) in ViewModel.
I'd suggest an approach to keep the ViewModel clean and get whatever data needed from View..
First, Define an interface in ViewModel's namespace, everything ViewModel wants from View will be defined in this interface..
public interface IUiServices
{
(string mouseX, string mouseY) GetMouseCoordinates();
}
Next, Let your Window (or UserControl) that hosts the <Canvas/> implement this interface
public partial class TheWindow : IUiServices {
// ..
private string _mouseX;
private string _mouseY;
public (string mouseX, string mouseY) GetMouseCoordinates() => (_mouseX, _mouseY);
}
Now, Let the Canvas subscribe to MouseMove event
<Canvas MouseMove="Canvas_OnMouseMove"
And add the handler to set the mouse coords variables
private void MainWindow_OnMouseMove(object sender, MouseEventArgs e)
{
Point position = e.GetPosition(sender as Canvas);
_mouseX = position.X.ToString();
_mouseY = position.Y.ToString();
}
Finally, you can register IUiServices in Prism
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
// ...
containerRegistry.RegisterSingleton<IUiServices, TheView>();
// ...
}
And inject it in ViewModel's constructor
public TheViewModel(.. , IUiServices uiServices){
//..
}
Now, wherever you want to get the coordinates, just call uiServices.GetMouseCoordinates().
Note1: From now on, any service ViewModel wants from View, just define it in IUiServices interface, implement it in View and use it in ViewModel
Note2: you might not use want to pass the service to the ViewModel via Prism, then you could inject it via setter injection
private IUiServices UiServices {set; get;}
public SetUiService(IUiServices s){
UiServices = s;
}
And in TheView, you can do the injection (DataContext as TheViewModel)?.SetUiService(this);
Note3: you can remove all of these from your code: the DelegateCommands in your ViewModel and all the code snippets you've there + <i:Interaction.Triggers/> code-block in your .xaml.

Related

WPF Complex Logic of Custom Controls with MVVM

I am creating a WPF-based plugin (for Revit, an architectural 3D modelling software, but this shouldn't matter) which is quite complex and I'm getting kind of lost.
The WPF Window is composed by 2 tabs and each Tab is a custom UserControl that I'm inserting in the TabItem through a Frame. The Main Window has a ViewModel where the data is bound.
One of the tabs helps with the creation of floors in a 3D model
part of MainWindow.xaml
<TabItem Name="LevelsTab" Header="Levels" HorizontalContentAlignment="Left">
<ScrollViewer >
<Frame Name="LevelsContent" Source="LevelsTab.xaml"/>
</ScrollViewer>
</TabItem>
The LevelsTab.xaml UserControl is really barebone and just contains buttons to create or remove a custom UserControl I created to represent graphically a floor in the UI (screenshot below). This very simple as well:
LevelDefinition.xaml
<UserControl x:Class="RevitPrototype.Setup.LevelDefinition" ....
<Label Grid.Column="0" Content="Level:"/>
<TextBox Name="LevelName" Text={Binding <!--yet to be bound-->}/>
<TextBox Name="LevelElevation" Text={Binding <!--yet to be bound-->}/>
<TextBox Name="ToFloorAbove" Text={Binding <!--yet to be bound-->}/>
</UserControl>
When the user clicks the buttons to add or remove floors in LevelsTab.xaml, a new LevelDefinition is added or removed to the gird.
Each LevelDefinition will be able to create a Level object from the information contained in the different TextBox elements, using MVVM. Eventually, in the ViewModel, I should have a List<Level> I guess.
Level.cs
class Level
{
public double Elevation { get; set; }
public string Name { get; set; }
public string Number { get; set; }
}
Each LevelDefinition should be sort of bound to the previous one though, as the floor below contains the information of the height to the Level above. The right-most TextBox in LevelDefinition.xaml indicated the distance between the current floor and the floor above, hence the Height `TextBox should just be the sum of its height PLUS the distance to the level above:
Of course the extra level of difficulty here is that if I change distance to the level above in one floor, all the floors above will have to update the height. For example: I change LEVEL 01 (from the pic) to have 4 meters to the level above, LEVEL 02's height will have to update to become 7m (instead of 6) and LEVEL 03's will have to become 10m.
But at this point I'm very lost:
How do I get this logic of getting the floor height bound to the info in the floor below?
How do I implement MVVM correctly in this case?
I hope I managed to explain the situation correctly even though it's quite complex and thanks for the help!
If you intend to make your Level items editable, you have to implement INotifyPropertyChanged. I created a level view model for demonstration purposes and added a property OverallElevation that represents the current elevation including that of previous levels.
public class LevelViewModel : INotifyPropertyChanged
{
private string _name;
private int _number;
private double _elevation;
private double _overallElevation;
public LevelViewModel(string name, int number, double elevation, double overallElevation)
{
Number = number;
Name = name;
Elevation = elevation;
OverallElevation = overallElevation;
}
public string Name
{
get => _name;
set
{
if (_name == value)
return;
_name = value;
OnPropertyChanged();
}
}
public int Number
{
get => _number;
set
{
if (_number == value)
return;
_number = value;
OnPropertyChanged();
}
}
public double Elevation
{
get => _elevation;
set
{
if (_elevation.CompareTo(value) == 0)
return;
_elevation = value;
OnPropertyChanged();
}
}
public double OverallElevation
{
get => _overallElevation;
set
{
if (_overallElevation.CompareTo(value) == 0)
return;
_overallElevation = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
You can bind these properties to your LevelDefinition user control. I adapted your sample, because it is incomplete. Since the overall elevation is calculated, I set the corresponding TextBox to be read-only, but you should really use a TextBlock or a similar read-only control instead.
<UserControl x:Class="RevitPrototype.Setup.LevelDefinition"
...>
<UserControl.Resources>
<Style TargetType="{x:Type TextBox}" BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="Margin" Value="5"/>
</Style>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="Level:"/>
<TextBox Grid.Column="1" Name="LevelName" Text="{Binding Name}"/>
<TextBox Grid.Column="2" Name="LevelElevation" Text="{Binding OverallElevation}" IsReadOnly="True"/>
<TextBox Grid.Column="3" Name="ToFloorAbove" Text="{Binding Elevation}"/>
</Grid>
</UserControl>
Since you did not provide your tab view model, I created one for reference. This view model exposes an ObservableCollection of levels, a GroundFloor property and commands to add and remove levels. I use a DelegateCommand type, but you may use a different one.
On each add of a level, you subscribe to the PropertyChanged event of the new level and on removal you unsubscribe to prevent memory leaks. Now, whenever a property changes on a LevelViewModel instance, the OnLevelPropertyChanged method is called. This method checks, if the Elevation property was changed. If it was, the UpdateOverallElevation method is called, which recalculates all overall elevation properties. Of course you could optimize this to only recalculate the levels above the current one passed as sender.
For a more robust implementation, you should subscribe to the CollectionChanged event of the Levels collection, so can subscribe to and unsubscribe from the PropertyChanged events of level items whenever you add, remove or modify the collection in other ways than through the commands like restoring a persisted collection.
public class LevelsViewModel
{
private const string GroundName = "GROUND FLOOR";
private const string LevelName = "LEVEL";
public ObservableCollection<LevelViewModel> Levels { get; }
public LevelViewModel GroundFloor { get; }
public ICommand Add { get; }
public ICommand Remove { get; }
public LevelsViewModel()
{
Levels = new ObservableCollection<LevelViewModel>();
GroundFloor = new LevelViewModel(GroundName, 0, 0, 0);
Add = new DelegateCommand<string>(ExecuteAdd);
Remove = new DelegateCommand(ExecuteRemove);
GroundFloor.PropertyChanged += OnLevelPropertyChanged;
}
private void ExecuteAdd(string arg)
{
if (!double.TryParse(arg, out var value))
return;
var lastLevel = Levels.Any() ? Levels.Last() : GroundFloor;
var number = lastLevel.Number + 1;
var name = GetDefaultLevelName(number);
var overallHeight = lastLevel.OverallElevation + value;
var level = new LevelViewModel(name, number, value, overallHeight);
level.PropertyChanged += OnLevelPropertyChanged;
Levels.Add(level);
}
private void ExecuteRemove()
{
if (!Levels.Any())
return;
var lastLevel = Levels.Last();
lastLevel.PropertyChanged -= OnLevelPropertyChanged;
Levels.Remove(lastLevel);
}
private void OnLevelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(LevelViewModel.Elevation))
return;
UpdateOverallElevation();
}
private static string GetDefaultLevelName(int number)
{
return $"{LevelName} {number:D2}";
}
private void UpdateOverallElevation()
{
GroundFloor.OverallElevation = GroundFloor.Elevation;
var previousLevel = GroundFloor;
foreach (var level in Levels)
{
level.OverallElevation = previousLevel.OverallElevation + level.Elevation;
previousLevel = level;
}
}
}
The view for the levels tab item could look like below. You can use a ListBox with your LevelDefinition user control as item template to display the levels. Alternatively, you could use a DataGrid with editable columns for each property of the LevelViewModel, which would be more flexible for users.
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListView ItemsSource="{Binding Levels}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<local:LevelDefinition/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListView>
<DockPanel Grid.Row="1" Margin="5">
<Button DockPanel.Dock="Right" Content="-" MinWidth="50" Command="{Binding Remove}"/>
<Button DockPanel.Dock="Right" Content="+" MinWidth="50" Command="{Binding Add}" CommandParameter="{Binding Text, ElementName=NewLevelElevationTextBox}"/>
<TextBox x:Name="NewLevelElevationTextBox" MinWidth="100"/>
</DockPanel>
<local:LevelDefinition Grid.Row="2" DataContext="{Binding GroundFloor}"/>
</Grid>
This is a simplified example, there is no input validation, invalid values are ignored on adding.
I've managed to implement this using a multi-binding converter.
Assuming that you set up the multi-converter as a static resource somewhere, the TextBlock to display the value is:
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource ElevationMultiConverter}">
<MultiBinding.Bindings>
<Binding Path="" />
<Binding Path="DataContext.Levels" RelativeSource="{RelativeSource AncestorType={x:Type ItemsControl}}" />
</MultiBinding.Bindings>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
The converter itself looks like this:
class ElevationMultiConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var item = values[0] as Level;
var list = values[1] as IList<Level>;
var lowerLevels = list.Where(listItem => list.IndexOf(listItem) <= list.IndexOf(item));
var elevation = lowerLevels.Sum(listItem => listItem.Height);
return elevation.ToString();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
In this example, it depends on the specific order of items in the list to determine whether a level is above or below another; you could use a property, or whatever else.
I didn't use a framework for this example so I needed to implement INotifyPropertyChanged everywhere myself. In the MainViewModel, this meant adding a listener to each Level element's PropertyChanged event to trigger the multibinding converter to have 'changed'. In total, my MainViewModel looked like this:
class MainViewModel :INotifyPropertyChanged
{
public ObservableCollection<Level> Levels { get; set; }
public MainViewModel()
{
Levels = new ObservableCollection<Level>();
Levels.CollectionChanged += Levels_CollectionChanged;
}
private void Levels_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
foreach(var i in e.NewItems)
{
(i as Level).PropertyChanged += MainViewModel_PropertyChanged;
}
}
private void MainViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Levels)));
}
public event PropertyChangedEventHandler PropertyChanged;
}
How it works:
A new Level is added to the collection, and it's PropertyChanged event is listened to by the containing view model. When the height of a level changes, the PropertyChanged event is fired and is picked up by the MainViewModel. It in turn fires a PropertyChanged event for the Levels property. The MultiConverter is bound to the Levels property, and all changes for it trigger the converters to re-evaluate and update all of the levels combined height values.

DataTemplate button command from binding

I'm using Wpf and I'm passing a List<Value> to a <ItemsControl> in the xaml. I would like to bind the string in the Value Object to the Command of a Button. This xaml part looks like this:
<Grid Margin="0,0,2,0">
<Grid Margin="10">
<ItemsControl Name="details">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="0,0,0,5">
<Grid.ColumnDefinitions>
....
</Grid.ColumnDefinitions>
...
<Button Grid.Column="2"
Content="{Binding ButtonContent}"
Visibility="{Binding ButtonVisibility}"
Command="{Binding ButtonClickMethod}" />
...
My Value Class looks like this:
public class Value
{
...
public string ButtonClickMethod { get; set; }
}
I'm setting the string link this:
v.ButtonClickMethod = "RelatedActivityId_OnClick";
And the Method is in the same class and looks like this:
private void RelatedActivityId_OnClick(object sender, RoutedEventArgs e)
{
MessageBox.Show("RelatedActivityId_OnClick");
}
Everything besides this is working properly and unses the same Object for the binding.
What am I doing wrong?
The Command property of the Button is of type ICommand so you cannot bind it to a string value.
You need to update your ButtonClickMethod to be of type ICommand or create a new property to bind you Command to.
See this answer for a sample implementation of ICommand.
If you need the button to execute code based on a parameter (string value?) then you can use the CommandParameter property, then use that paramters in your Command handler.
public class Value
{
public Value()
{
ButtonCommand = new RelayCommand((a) => true, CommandMethod);
}
public RelayCommand ButtonCommand {get; set; }
public string ButtonClickMethod { get; set; }
private void CommandMethod(object obj)
{
MessageBox.Show(obj?.ToString());
}
}
and the XAML:
<Button Grid.Column="2"
Content="{Binding ButtonContent}"
Visibility="{Binding ButtonVisibility}"
Command="{Binding ButtonCommand}"
CommandParameter="{Binding ButtonClickMethod}" />
The Button.Command property binds only to objects which implement the ICommand interface.
If you want to invoke a method which its name is ButtonClickMethod, you will have to:
Create a class which implements ICommand interface.
Create a object of that class and bind it to your button (bind it to Button.Command).
Pass the Value.ButtonClickMethod as a CommandParameter to your ICommand object.
Use this to invoke any method you would like to.

How to know which element is tapped in ListView? [duplicate]

This question already has answers here:
How to get clicked item in ListView
(5 answers)
Closed 7 years ago.
I've got a ListView with a DataTemplate like this, using MVVM pattern
<ListView ItemsSource="{Binding Source}"
IsItemClickEnabled="True"
commands:ItemsClickCommand.Command="{Binding ItemClickedCommand}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding A}" />
<Button Content="{Binding B}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
ItemsClickCommand is defined in this way
public static class ItemsClickCommand
{
public static readonly DependencyProperty CommandProperty = DependencyProperty.RegisterAttached("Command", typeof(BindableCommand), typeof(ItemsClickCommand), new PropertyMetadata(null, OnCommandPropertyChanged));
public static void SetCommand(DependencyObject d, BindableCommand value)
{
d.SetValue(CommandProperty, value);
}
public static BindableCommand GetCommand(DependencyObject d)
{
return (BindableCommand)d.GetValue(CommandProperty);
}
private static void OnCommandPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var control = d as ListViewBase;
if (control != null)
control.ItemClick += OnItemClick;
}
private static void OnItemClick(object sender, ItemClickEventArgs e)
{
var control = sender as ListViewBase;
var command = GetCommand(control);
if (command != null && command.CanExecute(e.OriginalSource))
command.ExecuteWithMoreParameters(e.OriginalSource, e.ClickedItem);
}
}
What I'm asking is how can I know if user tap on the TextBlock or Button.
I tried to handle ItemClickCommand event in this way in ViewModel to search controls in VisualTree (is this the best solution?), but the cast to DependencyObject doesn't work (returns always null)
public void ItemClicked(object originalSource, object clickedItem)
{
var source = originalSourceas DependencyObject;
if (source == null)
return;
}
There are a few solutions that come to mind
Solution 1
<ListView
x:Name="parent"
ItemsSource="{Binding Source}"
IsItemClickEnabled="True"
Margin="20">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding A}" />
<Button
Content="{Binding B}"
Command="{Binding DataContext.BCommand, ElementName=parent}"
CommandParameter="{Binding}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Note how the ListView has the name set to "parent" with the attribute: x:Name="parent" and how the binding for the button's command uses that. Also note that the command will be provided with a parameter that is the reference to the data source for the element that was clicked.
The view model for this page will look like this:
public class MainViewModel : MvxViewModel
{
public ObservableCollection<MySource> Source { get; private set; }
public MvxCommand<MySource> BCommand { get; private set; }
public MainViewModel()
{
Source = new ObservableCollection<MySource>()
{
new MySource("e1", "b1"),
new MySource("e2", "b2"),
new MySource("e3", "b3"),
};
BCommand = new MvxCommand<MySource>(ExecuteBCommand);
}
private void ExecuteBCommand(MySource source)
{
Debug.WriteLine("ExecuteBCommand. Source: A={0}, B={1}", source.A, source.B);
}
}
'MvxCommand' is just a particular implementation of ICommand. I used MvvMCross for my sample code but you don't have to do that - you can use whatever MvvM implementation you need.
This solution is appropriate if the responsibility to handle the command lies with the view model for the page that contains the list.
Solution 2
Handling the command in the view model for the page that contains the list may not always be appropriate. You may want to move that logic in code that is closer to the element that is being clicked. In that case, isolate the data template for the element in its own user control, create a view model class that corresponds to the logic behind that user control and implement the command in that view model. Here is how the code would look like:
The XAML for the ListView:
<ListView
ItemsSource="{Binding Source}"
IsItemClickEnabled="True"
Margin="20">
<ListView.ItemTemplate>
<DataTemplate>
<uc:MyElement DataContext="{Binding Converter={StaticResource MySourceToMyElementViewModelConverter}}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
The XAML for the user control representing one element:
<Grid>
<StackPanel>
<TextBlock Text="{Binding Source.A}" />
<Button Content="{Binding Source.B}" Command="{Binding BCommand}" />
</StackPanel>
</Grid>
The source code for MySourceToMyElementViewModelConverter:
public class MySourceToMyElementViewModelConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
return new MyElementViewModel((MySource)value);
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotSupportedException();
}
}
The view model for the main page:
public class MainViewModel : MvxViewModel
{
public ObservableCollection<MySource> Source { get; private set; }
public MainViewModel()
{
Source = new ObservableCollection<MySource>()
{
new MySource("e1", "b1"),
new MySource("e2", "b2"),
new MySource("e3", "b3"),
};
}
}
The view model for the user control representing one element in the list:
public class MyElementViewModel : MvxViewModel
{
public MySource Source { get; private set; }
public MvxCommand BCommand { get; private set; }
public MyElementViewModel(MySource source)
{
Source = source;
BCommand = new MvxCommand(ExecuteBCommand);
}
private void ExecuteBCommand()
{
Debug.WriteLine("ExecuteBCommand. Source: A={0}, B={1}", Source.A, Source.B);
}
}
Solution 3
Your sample assumes that the view model for the main page exposes a list of data model elements. Something like this:
public ObservableCollection<MySource> Source { get; private set; }
The view model for the main page could be changed so that it exposes a list of view model elements instead. Something like this:
public ObservableCollection<MyElementViewModel> ElementViewModelList { get; private set; }
Each element in ElementViewModelList would correspond to an element in Source. This solution can get slightly complex if the contents of Source changes at run time. The view model of the main page will need to observe Source and change ElementViewModelList accordingly. Going further don this path you may want to abstract the concept of a collection mapper (something similar with an ICollectionView) and provide some generic code for doing so.
For this solution, the XAML will look like this:
<ListView
ItemsSource="{Binding ElementViewModelList}"
IsItemClickEnabled="True"
Margin="20">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding A}" />
<Button Content="{Binding B}" Command="{Binding BCommand}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Notes for Solution 1, 2 and 3
I see that your original sample associates a commanding not with the button inside of the element but with the entire element. That raises the question: what are you going to do with the inner button? Will you have a situation where the user can click either on the element or on the inner button? That may not be the best solution as far as UI/UX goes. Be mindful of that. Just as an exercise and in order to get closer to your original sample, here is what you can do if you want to associate commanding with the entire element.
Wrap your entire element in a button with a custom style. That style will modify the way a click is handled visually. The simplest form of that is to have the click not create any visual effect. This change applied to Solution 1 (it can easily be applied to Solution 2 and Solution 3 as well) would look something like this:
<ListView
x:Name="parent"
ItemsSource="{Binding Source}"
IsItemClickEnabled="True"
Margin="20">
<ListView.ItemTemplate>
<DataTemplate>
<Button
Command="{Binding DataContext.BCommand, ElementName=parent}"
CommandParameter="{Binding}"
Style="{StaticResource NoVisualEffectButtonStyle}">
<StackPanel>
<TextBlock Text="{Binding A}" />
<TextBlock Text="{Binding B}" />
</StackPanel>
</Button>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
In this case you would have to write NoVisualEffectButtonStyle but that is a simple task. You would also need to decide what kind of commanding you want to associate with the inner button (otherwise why would you have an inner button). Or, more likely you could transform the inner button in something like a textbox.
Solution 4
Use Behaviors.
First, add a reference to "Behaviors SDK".. Then modify your XAML code:
...
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:core="using:Microsoft.Xaml.Interactions.Core"
...
<Grid>
<ListView ItemsSource="{Binding Source}" IsItemClickEnabled="True" Margin="20">
<interactivity:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="ItemClick">
<core:InvokeCommandAction
Command="{Binding BCommand}"
InputConverter="{StaticResource ItemClickedToMySourceConverter}" />
</core:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding A}" />
<TextBlock Text="{Binding B}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
ItemClickedToMySourceConverter is just a normal value converter:
public class ItemClickedToMySourceConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
return (MySource)(((ItemClickEventArgs)value).ClickedItem);
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotSupportedException();
}
}
The view model will look like this:
public class Main4ViewModel : MvxViewModel
{
public ObservableCollection<MySource> Source { get; private set; }
public MvxCommand<MySource> BCommand { get; private set; }
public Main4ViewModel()
{
Source = new ObservableCollection<MySource>()
{
new MySource("e1", "b1"),
new MySource("e2", "b2"),
new MySource("e3", "b3"),
};
BCommand = new MvxCommand<MySource>(ExecuteBCommand);
}
private void ExecuteBCommand(MySource source)
{
Debug.WriteLine("ExecuteBCommand. Source: A={0}, B={1}", source.A, source.B);
}
}

How do I get mouse positions in my view model

From MVVM Design pattern, the viewmodel should not know the view. But in my case, I need the view and the model, I mean :
In my window, I've an Image component. I'd like to get mouse position when mouse moves over the Image component and save it into my model.
The code behind would have been :
void Foo_MouseMove(objet sender, MouseEventArgs e)
{
model.x = e.getPosition(this.imageBox).X;
model.y = e.getPosition(this.imageBox).Y;
}
The problem is : I need this.imageBox and MouseEventArgs, so two View element.
My question is : How to deal with this case using the MVVM approach ?
I use MVVM light framework
I would use an attached behaviour here. This will allow you to continuously monitor the mouse position, rather than simply responding to an event such as MouseDown. You'll need to add a reference to the System.Windows.Interactivity assembly.
The code below provides a simple example of this in action.
XAML
<Window x:Class="MouseMoveMvvm.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:mouseMoveMvvm="clr-namespace:MouseMoveMvvm"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<TextBlock Text="{Binding PanelX, StringFormat='X={0}'}" />
<TextBlock Text="{Binding PanelY, StringFormat='y={0}'}" />
</StackPanel>
<Canvas DockPanel.Dock="Bottom" Background="Aqua">
<i:Interaction.Behaviors>
<mouseMoveMvvm:MouseBehaviour MouseX="{Binding PanelX, Mode=OneWayToSource}" MouseY="{Binding PanelY, Mode=OneWayToSource}" />
</i:Interaction.Behaviors>
</Canvas>
</DockPanel>
</Grid>
</Window>
Note that, in the above XAML, the MouseBehaviour is pushing the mouse position down to the ViewModel through a OneWayToSource binding, while the two TextBlocks are reading the mouse positions from the ViewModel.
ViewModel
public class MainWindowViewModel : INotifyPropertyChanged
{
private double _panelX;
private double _panelY;
public event PropertyChangedEventHandler PropertyChanged;
public double PanelX
{
get { return _panelX; }
set
{
if (value.Equals(_panelX)) return;
_panelX = value;
OnPropertyChanged();
}
}
public double PanelY
{
get { return _panelY; }
set
{
if (value.Equals(_panelY)) return;
_panelY = value;
OnPropertyChanged();
}
}
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Attached Behaviour
public class MouseBehaviour : System.Windows.Interactivity.Behavior<FrameworkElement>
{
public static readonly DependencyProperty MouseYProperty = DependencyProperty.Register(
"MouseY", typeof (double), typeof (MouseBehaviour), new PropertyMetadata(default(double)));
public double MouseY
{
get { return (double) GetValue(MouseYProperty); }
set { SetValue(MouseYProperty, value); }
}
public static readonly DependencyProperty MouseXProperty = DependencyProperty.Register(
"MouseX", typeof(double), typeof(MouseBehaviour), new PropertyMetadata(default(double)));
public double MouseX
{
get { return (double) GetValue(MouseXProperty); }
set { SetValue(MouseXProperty, value); }
}
protected override void OnAttached()
{
AssociatedObject.MouseMove += AssociatedObjectOnMouseMove;
}
private void AssociatedObjectOnMouseMove(object sender, MouseEventArgs mouseEventArgs)
{
var pos = mouseEventArgs.GetPosition(AssociatedObject);
MouseX = pos.X;
MouseY = pos.Y;
}
protected override void OnDetaching()
{
AssociatedObject.MouseMove -= AssociatedObjectOnMouseMove;
}
}
Finnally found an answer, using a EventConverter :
public class MouseButtonEventArgsToPointConverter : IEventArgsConverter
{
public object Convert(object value, object parameter)
{
var args = (MouseEventArgs)value;
var element = (FrameworkElement)parameter;
var point = args.GetPosition(element);
return point;
}
}
This converter allows me to deal with Point and not with graphics components.
Here goes the XML :
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseMove">
<cmd:EventToCommand
Command="{Binding Main.MouseMoveCommand, Mode=OneWay}"
EventArgsConverter="{StaticResource MouseButtonEventArgsToPointConverter}"
EventArgsConverterParameter="{Binding ElementName=Image1}"
PassEventArgsToCommand="True" />
</i:EventTrigger>
</i:Interaction.Triggers>
Mark Greens solution is the best (I found).
If you want to make his solution reusable for any WPF control (which I suggest), inheriting from System.Windows.Interactivity.Behavior<Control> actually won't work for Panel, because Panel does not inherit from Control.
Only those classes inherit from Control:
https://msdn.microsoft.com/de-de/library/system.windows.controls.control(v=vs.110).aspx
Instead, inherit from System.Windows.Interactivity.Behavior<FrameworkElement>. FrameworkElement is the ancestor of all WPF control classes: https://msdn.microsoft.com/de-de/library/system.windows.frameworkelement(v=vs.110).aspx.
I have tested it on Grid, Panel and Image btw.
I use it to keep a Popup in sync with the mouse cursor:
<Image x:Name="Image1">
<i:Interaction.Behaviors>
<myNamespace:MouseBehaviour
MouseX="{Binding ElementName=Popup1, Path=HorizontalOffset, Mode=OneWayToSource}"
MouseY="{Binding ElementName=Popup1, Path=VerticalOffset, Mode=OneWayToSource}">
</myNamespace:MouseBehaviour>
</i:Interaction.Behaviors>
</Image>
<Popup x:Name="Popup1" PlacementTarget="{Binding ElementName=Image1}"/>
P.S.: I would have commented on the solution, but my answer is too long.

How to automatically change text in a silverlight textbox?

I am using MVVM/Caliburn.Micro in a silverlight 5 project and I have a requirement to automatically change the text the user enters in a silverlight textbox to uppercase.
First, I thought I could just set the backing variable on the ViewModel to uppercase and the two way binding would change the text. That didn't work (though I believe it will if I use a lost focus event, but I cannot do that since I have other things I must do for KeyUp as well and attaching two events results in a xaml error)
Since that didn't work I tried calling a method on the KeyUp event. This technically works, but since it is replacing the text it puts the cursor back at the beginning, so the user ends up typing backwards.
This seems like fairly simple functionality - how do I transform the text a user types into uppercase? Am I missing something easy?
Here is my existing code. Xaml:
<TextBox x:Name="SomeName" cal:Message.Attach="[Event KeyUp] = [Action ConvertToUppercase($eventArgs)]" />
View Model:
public void ConvertToUppercase(System.Windows.Input.KeyEventArgs e)
{
SomeName = _someName.ToUpper();
//Other code that doesn't concern uppercase
}
EDIT FOR ALTERNATE SOLUTION:
McAden put forth a nice generic solution. I also realized at about the same time that there was an alternate solution (just pass the textbox as a param to the uppercase method and move the cursor), so here is the code for that as well:
xaml:
<TextBox x:Name="SomeName" cal:Message.Attach="[Event KeyUp] = [Action ConvertToUppercase($source, $eventArgs)]; [Event KeyDown] = [Action DoOtherStuffThatIsntQuestionSpecific($eventArgs)]" />
cs method:
public void ConvertToUppercase(TextBox textBox, System.Windows.Input.KeyEventArgs e)
{
//set our public property here again so the textbox sees the Caliburn.Micro INPC notification fired in the public setter
SomeName = _someName.ToUpper();
//move the cursor to the last so the user can keep typing
textBox.Select(SomeName.Length, 0);
}
and of course cs standard Caliburn.Micro property:
private String _someName = "";
public String SomeName
{
get
{
return _someName;
}
set
{
_someName = value;
NotifyOfPropertyChange(() => SomeName);
}
}
Create a ToUpper EventTrigger as mentioned here. Also create another one for whatever otherfunctionality you're trying to accomplish. Add them both in xaml:
<TextBox Text="{Binding SomeName, Mode=TwoWay}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="TextChanged">
<myBehaviors:UpperCaseAction/>
</i:EventTrigger>
<i:EventTrigger EventName="TextChanged">
<myBehaviors:MyOtherAction/>
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>
EDIT: I've fully tested this solution using the following (NO code-behind is involved)
UpperCase Action:
public class UpperCaseAction : TriggerAction<TextBox>
{
protected override void Invoke(object parameter)
{
var selectionStart = AssociatedObject.SelectionStart;
var selectionLength = AssociatedObject.SelectionLength;
AssociatedObject.Text = AssociatedObject.Text.ToUpper();
AssociatedObject.SelectionStart = selectionStart;
AssociatedObject.SelectionLength = selectionLength;
}
}
Other Action:
public class OtherAction : TriggerAction<TextBox>
{
Random test = new Random();
protected override void Invoke(object parameter)
{
AssociatedObject.FontSize = test.Next(9, 13);
}
}
XAML namespaces (TestSL in this case being the namespace of my test project - use your namespace as appropriate):
xmlns:local="clr-namespace:TestSL"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
XAML TextBox
<Grid x:Name="LayoutRoot" Background="LightGray" Width="300" Height="200">
<TextBox TextWrapping="Wrap" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="10" Width="100">
<i:Interaction.Triggers>
<i:EventTrigger EventName="TextChanged">
<local:UpperCaseAction />
</i:EventTrigger>
<i:EventTrigger EventName="TextChanged">
<local:OtherAction />
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>
</Grid>
UpperCaseConverter.cs:
namespace MyProject.Converters
{
/// <summary>
/// A upper case converter for string values.
/// </summary>
public class UpperCaseConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return ConvertToUpper(value);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return ConvertToUpper(value);
}
private string ConvertToUpper(object value)
{
if (value != null)
{
return value.ToString().ToUpper();
}
return null;
}
}
}
AppResources.xaml:
<ResourceDictionary
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"
xmlns:conv="clr-namespace:MyProject.Converters;assembly=MyProject"
mc:Ignorable="d"
>
<!-- Converters -->
<conv:UpperCaseConverter x:Key="UpperCaseConverter"/>
</ResourceDictionary>
MyFormView.xaml:
<UserControl>
<TextBox Text="{Binding myText, Mode=TwoWay, Converter={StaticResource UpperCaseConverter}}" />
</UserControl>

Categories