I am attempting to add command capability to a ComboBox. After some searching, I decided on the following approach as being the simplist:
1) Add System.Windows.Interactivity.dll to my References
2) Add the following to my XAML
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
3) Add the following to my ComboBox
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding ChangePlanner}" />
</i:EventTrigger>
</i:Interaction.Triggers>
I have two questions:
A) Is this the most straightforward approach? If not, what is?
B) If this is the right approach, why does it not work? That is, my ChangePlanner Sub is not being invoked.
Here is a quick working sample using the triggers with a ComboBox:
ViewModel
public class ShellViewModel : BindableBase
{
private string _selectedItem;
public string Title => "Sample";
public ObservableCollection<string> Items
{
get;
} = new ObservableCollection<string>(new[] { "A", "B", "C" });
public string SelectedItem
{
get => _selectedItem;
set => SetProperty(ref _selectedItem, value);
}
public ICommand ChangeCommand => new DelegateCommand<string>(s => Debug.WriteLine($"Command Executed: {s}"));
}
View
<Window x:Class="Poc.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:Poc.ViewModels"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
mc:Ignorable="d"
Title="{Binding Title}" Height="350" Width="525">
<Window.DataContext>
<viewModels:ShellViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<ComboBox Grid.Row="0" ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding ChangeCommand}" CommandParameter="{Binding SelectedItem}"></i:InvokeCommandAction>
</i:EventTrigger>
</i:Interaction.Triggers>
</ComboBox>
</Grid>
Haven't seen your code posted yet, but I am going to guess that you were trying to bind to a method (and not an ICommand).
Is this the most straightforward approach? If not, what is?
The most straightforward and MVVM friendly approach would be to bind the SelectedItem of the ComboBox to a source property of your view model and handle any logic, or invoke the command, in the setter of this one:
private object _selectedItem;
public object SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
ChangePlanner.Execute(null);
}
}
why does it not work?
Impossible to say based on the information you have provided. Make sure that ChangePlanner is a public property of the DataContext of the ComboBox that returns an ICommand to begin with.
Related
A simple example to reproduce the problem:
namespace IncorrectAlternativeIndex
{
/// <summary>A simple class to demonstrate the problem</summary>
public class Point2D
{
public double X { get; set; }
public double Y { get; set; }
}
}
namespace IncorrectAlternativeIndex
{
/// <summary>A simple ViewModel to demonstrate the problem</summary>
public class PointsViewModel : ViewModelBase
{
public ObservableCollection<Point2D> Points { get; } = new();
public RelayCommand ClearPoints => GetCommand(Points.Clear);
/// <summary>This command should not exist.
/// I brought it only to demonstrate the problem. </summary>
public RelayCommand Refresh => GetCommand<CollectionView>(cv => cv.Refresh());
}
}
<Window x:Class="IncorrectAlternativeIndex.CheckAlternativeIndexWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:IncorrectAlternativeIndex"
xmlns:sys="clr-namespace:System;assembly=netstandard"
mc:Ignorable="d"
Title="CheckAlternativeIndexWindow" Height="450" Width="800"
DataContext="{DynamicResource vm}">
<Window.Resources>
<local:PointsViewModel x:Key="vm"/>
</Window.Resources>
<UniformGrid Rows="1">
<DataGrid x:Name="dGrid" ItemsSource="{Binding Points}"
AlternationCount="{x:Static sys:Int32.MaxValue}">
<DataGrid.RowHeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=(ItemsControl.AlternationIndex),
RelativeSource={RelativeSource FindAncestor,
AncestorType=DataGridRow}}"/>
</DataTemplate>
</DataGrid.RowHeaderTemplate>
</DataGrid>
<UniformGrid Columns="1">
<Button Content="Clear" Margin="50" Command="{Binding ClearPoints}"/>
<Button Content="Refresh" Margin="50" Command="{Binding Refresh}"
CommandParameter="{Binding Items, ElementName=dGrid}"/>
</UniformGrid>
</UniformGrid>
</Window>
When adding elements to an empty collection by typing into a new row directly in the DataGrid, the AlternationIndex value starts at AlternationCount - 1.
If do a Refresh of the collection view, the correct AlternationIndex calculation is restored.
After cleaning the collection, everything is repeated in the same way.
Is there a way to fix this?
P.S. Just in case, I clarify that I do not need line numbers in the source data. It is required only in the View for the convenience of the user.
I'm using MVVM Light in my WPF application.
I created a class RedirectToUriCommandArgument.cs.
public class RedirectToUriCommandArgument : DependencyObject
{
#region Properties
public static readonly DependencyProperty PageProperty =
DependencyProperty.Register(nameof(Page), typeof(object), typeof(RedirectToUriCommandArgument), new UIPropertyMetadata(null));
public object Page
{
get => (object)GetValue(PageProperty);
set => SetValue(PageProperty, value);
}
public string Uri { get; set; }
#endregion
#region Methods
#endregion
}
In .xaml file, I used:
<Window x:Class="MainClient.Views.AppView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:v="clr-namespace:MainClient.Views"
xmlns:vm="clr-namespace:MainClient.ViewModel"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:commandArgument="clr-namespace:MainClient.Models.CommandArguments"
xmlns:local="clr-namespace:MainClient"
mc:Ignorable="d"
WindowStartupLocation="CenterScreen"
Height="350" Width="525">
<Window.DataContext>
<vm:AppViewModel x:Name="AppContext"></vm:AppViewModel>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Frame NavigationUIVisibility="Hidden" x:Name="PageFrame">
<Frame.Content>
<Page Name="MainPage"></Page>
</Frame.Content>
</Frame>
<StackPanel Grid.Row="1" Orientation="Horizontal">
<Button>
<Button.Content>
<TextBlock>Redirect to main view</TextBlock>
</Button.Content>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<i:InvokeCommandAction Command="{Binding RedirectToViewRelayCommand}">
<i:InvokeCommandAction.CommandParameter>
<commandArgument:RedirectToUriCommandArgument Page="{Binding ElementName=PageFrame}" Uri="MainView.xaml"></commandArgument:RedirectToUriCommandArgument>
</i:InvokeCommandAction.CommandParameter>
</i:InvokeCommandAction>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</StackPanel>
</Grid>
</Window>
The Page property is always null.
Am I missing anything ?
So I think the problem is, that as binding been initialized the UIElement is not created(null). Afterwords the binding is not notified, that the object is created.
Binding to the properties is easyer the object must implement INotifyPropertyChanged or DependencyObject take care about dependency properties.
To solve your issue you could set a Delay for Binding, say to 1000ms, then it will work. It's doubtful whether it is a right way.
<commandArgument:RedirectToUriCommandArgument Page="{Binding ElementName=PageFrame, Delay=1000}" Uri="MainView.xaml"></commandArgument:RedirectToUriCommandArgument>
The right way would be just set binding's source to the UIElement:
<commandArgument:RedirectToUriCommandArgument Page="{Binding Source={x:reference PageFrame}}" Uri="MainView.xaml"></commandArgument:RedirectToUriCommandArgument>
I have a view, which initializes a viewmodel inside the windows resources. Further more I give my grid the DataContext.
My question is, how I can add a command to my windows closing event keeping mvvm in memory? I tried the version of this post:
Handling the window closing event with WPF / MVVM Light Toolkit
... but its not working using an event-trigger, because I can't access the viewmodel from outside my grid, so I can't access my command.
Any solution for my problem?
Greetings
Jannik
Edit: Here's my xaml:
<Window x:Class="WpfApplication1.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModels="clr-namespace:WpfApplication1.ViewModels"
xmlns:converter="clr-namespace:WpfApplication1.Converter"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<viewModels:MainWindowViewModel x:Key="ViewModel"/>
</Window.Resources>
<Grid DataContext="{StaticResource ViewModel}">...</Grid>
</Window>
You can reference to members of a static resource this way:
Command="{Binding Path=CloseCommand, Source={StaticResource ViewModel}}"
Here's the complete test project. I used a text box with a binding to ensure data is saved.
<Window x:Class="WpfApplication1.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModels="clr-namespace:WpfApplication1.ViewModels"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<viewModels:MainWindowViewModel x:Key="ViewModel"/>
</Window.Resources>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Closing">
<i:InvokeCommandAction Command="{Binding Path=CloseCommand, Source={StaticResource ViewModel}}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<Grid DataContext="{StaticResource ViewModel}">
<TextBox Text="{Binding Txt, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
</Window>
In ViewModel code, I used a static reference to store data (LastInstance). you can replace it with your own method.
Also I used Command which is a custom implementation of ICommand. If you want I can add the complete implementation here.
public MainWindowViewModel()
{
//save or load here...
if (LastInstance != null) Txt = LastInstance.Txt;
CloseCommand = new Command(o => LastInstance = this);
//...
}
public static ViewModel LastInstance;
//Txt Dependency Property
public string Txt
{
get { return (string)GetValue(TxtProperty); }
set { SetValue(TxtProperty, value); }
}
public static readonly DependencyProperty TxtProperty =
DependencyProperty.Register("Txt", typeof(string), typeof(ViewModel), new UIPropertyMetadata(null));
//CloseCommand Dependency Property
public Command CloseCommand
{
get { return (Command)GetValue(CloseCommandProperty); }
set { SetValue(CloseCommandProperty, value); }
}
public static readonly DependencyProperty CloseCommandProperty =
DependencyProperty.Register("CloseCommand", typeof(Command), typeof(ViewModel), new UIPropertyMetadata(null));
The typical approach to this problem is to have a MainViewModel and set the DataContext of you Window to it. Then define other viewModels in the MainViewModel.
<Window>
<Grid DataContext="{Binding MyGridViewModel}">
</Grid>
<DockPanel DataContext="{Binding AnotherViewModel}">
</DockPanel>
</Window>
in MainWindow constructor:
this.DataContext = new MainViewModel();
in MainViewModel constructor:
this.MyGridViewModel = new OtherViewModel();
This way you have many options to find the desired object through viewModel references.
So I have a WPF UserControl:
<UserControl x:Class="BI_Builder.Views.ObjectTreeView"
x:Name="UC1"
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:local="clr-namespace:BI_Builder"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:viewModels="clr-namespace:BI_Builder.ViewModels"
xmlns:command="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WPF4"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300" DataContext="{Binding}">
<UserControl.Resources>
<ContentControl x:Key="Context" Content="{Binding}" />
<DataTemplate x:Key="DataSourceTemplate">
<TextBlock Text="{Binding Path=Name}" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<command:EventToCommand Command="{Binding Path=DataContext.OpenCommand, Mode=OneWay,ElementName=UC1}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBlock>
</DataTemplate>
<HierarchicalDataTemplate x:Key="ItemTemplate"
ItemsSource="{Binding Children}"
ItemTemplate="{StaticResource DataSourceTemplate}">
<StackPanel>
<TextBlock Text="{Binding Header}">
</TextBlock>
</StackPanel>
</HierarchicalDataTemplate>
</UserControl.Resources>
<Grid>
<TreeView Name="TreeView" ItemsSource="{Binding Items}" ItemTemplate="{StaticResource ItemTemplate}" >
</TreeView>
</Grid>
</UserControl>
And here's the main view model for the user control:
public class ObjectTreeViewModel : ObservableObject {
public ObservableCollection<ItemViewModel> Items {
get {
if (_items != null) return _items;
_items = new ObservableCollection<ItemViewModel>();
_items.Add(DataSources);
return _items;
}
set { _items = value;
}
}
public ItemViewModel DataSources {
get { return _dataSources ?? (_dataSources = new ItemViewModel() { Header = "Data Sources", Children = new ObservableCollection<object>(DataSourceList) }); }
set { _dataSources = value; }
}
public List<DataSource> DataSourceList;
public ICommand OpenCommand {
get { if (_openCommand == null) { return _openCommand = new RelayCommand(OpenDataSource); } return _openCommand; }
}
private void OpenDataSource() {
MessageBox.Show("Test");
}
public ObjectTreeViewModel() {
DataSourceList = new List<DataSource>();
DataSourceList.Add(new DataSource() { Name = "Test" });
}
private ItemViewModel _dataSources;
private ObservableCollection<ItemViewModel> _items;
private RelayCommand _openCommand;
}
}
I've tried every method I've come across on the web to get the EventToCommand in the DataSourceTemplate DataTemplate to fire. In fact, I'm pretty sure it knows where the OpenCommand is, because if I change the Path to gobbledygook, the Output window throws me an error saying that "ObjectTreeView" (which is the instance of the ObjectTreeViewModel view model being bound to the UserControl) doesn't have the gobbledygook property. So I think I've set the DataContext correctly ...
But whenever I click on the text blocks ... nothing.
Really trying to avoid code-behind (it just feels wrong), and full disclosure, I'm using MVVM Light's EventToCommand but not the full toolkit, although I'm tempted to rewrite what I have so far in it to see if using the Service Locator will solve this problem.
The TextBlock control does not have a Click event. See MSDN.
You should use the MouseLeftButtonDown event instead:
<i:EventTrigger EventName="MouseLeftButtonDown">
<!-- ... -->
</i:EventTrigger>
Can you put a hyperlink inside your textblock instead and bind the command to the hyperlink?
Note you can style the hyperlink to look like a plain textblock if needed.
<TextBlock>
<Hyperlink Command="{Binding Path=DataContext.OpenCommand" Text="{Binding Path=Name}" />
</TextBlock>
Also make sure that the ObjectTreeView class is instantiated and loaded into DataContext of the usercontrol.
I'm new to MVVM and Silverlight and I'm just trying to figure out a simple scenario.
I'm using the MVVM Light toolkit and Silverlight 3.0 without Expression Blend.
I have a DataGrid and a DataForm bound to an observable collection in a ViewModel. I would like to bind to my RelayCommand Save() property after I make changes to the data in the DataForm control and have this accomplished without using the code behind for my view.
The DataForm doesn't use the cmd:ButtonBaseExtensions.Command that MVVM Light uses for normal button click command binding, so I'm not sure how to tie the control to my ViewModel.
Any help is appreciated!
I figured it out shortly after posting the question. Go figure.
When using the MVVM Light Toolkit you can bind to events using the EventToCommand feature.
My Xaml looks like this:
<UserControl x:Class="CountyBusinessDirectory.UI.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:dataFormToolkit="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data.DataForm.Toolkit"
xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight"
xmlns:cmdextras="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras"
DataContext="{Binding BusinessesViewModel, Source={StaticResource Locator}}">
<Grid x:Name="LayoutRoot" ShowGridLines="False">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<data:DataGrid x:Name="dgAllBusinesses" CanUserSortColumns="True"
IsReadOnly="True" AutoGenerateColumns="True"
ItemsSource="{Binding Businesses}"
Grid.Column="0">
</data:DataGrid>
<ScrollViewer x:Name="svScroll" Grid.Column="1" >
<dataFormToolkit:DataForm x:Name="dfDetails"
ItemsSource="{Binding Businesses}"
AutoGenerateFields="True"
CommitButtonContent="Save"
CommandButtonsVisibility="Edit, Navigation, Commit, Cancel" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="EditEnded">
<cmdextras:EventToCommand Command="{Binding SaveBusiness}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</dataFormToolkit:DataForm>
</ScrollViewer>
</Grid>
And my ViewModel looks like this (using direct silverlight enabled WCF service in ViewModel for quick example, would normally pull this into an interface to decouple):
//using statements ommitted for brevity
namespace MyProject.ViewModels
{
public class BusinessesViewModel : ViewModelBase
{
private PagedCollectionView _businesses;
DALServiceClient _proxy;
public RelayCommand SaveBusiness
{ get; private set; }
public PagedCollectionView Businesses
{
get
{
return _businesses;
}
set
{
if (_businesses != value)
{
_businesses = value;
base.RaisePropertyChanged("Businesses");
}
}
}
public BusinessesViewModel()
{
_proxy = new DALServiceClient(); //Data Access Layer WCF Service
_proxy.GetBusinessesCompleted += new EventHandler<GetBusinessesCompletedEventArgs>(_proxy_GetBusinessesCompleted);
_proxy.GetBusinessesAsync();
SaveBusiness = new RelayCommand(() => SaveBusinessToDB());
}
private void SaveBusinessToDB()
{
Business bus = Businesses.CurrentItem as Business;
_proxy.UpdateBusinessesAsync(bus);
}
void _proxy_GetBusinessesCompleted(object sender, GetBusinessesCompletedEventArgs e)
{
if (e.Result != null)
{
Businesses = new PagedCollectionView(e.Result);
}
}
}
}