I have a simple WPF application:
View:
...
<Window.InputBindings>
<KeyBinding Key="D0" Command="{Binding MyCommand}" />
</Window.InputBindings>
<Grid>
<Button Command="{Binding MyCommand}" />
</Grid>
...
ViewModel:
public class MyViewModel : ReactiveObject
{
public ICommand MyCommand { get; }
public MyViewModel()
{
MyCommand = ReactiveCommand.Create(() => {});
}
}
Problem: When I click on the button or press D0 key I have 'The calling thread cannot access this object because a different thread owns it.' exception. But in case if I remove the button and press on D0 it's OK and command successfully completes. And there is the same result with the async command.
How should I bind this command to my button or what I do wrong?
Related
I am pretty new to WPF and MVVM so this may be a very easy question. I have an app with a button and a checkbox. Once the button is clicked it runs a command that then runs a script. The checkbox is an option to view an internet browser as the script runs. I am wondering how I can pass in wheather the checkbox is checked or not once the button is selected. I changed some of the coding names to be more basic. Here is my Xaml:
<StackPanel Margin="10">
<CheckBox Content="Option" IsChecked="True" />
<Button Height="20"
Content="Run Script"
Command="{Binding Script }"
/>
</StackPanel>
And here is the the ViewModel:
class MainWindowViewModel
{
public ICommand script{ get; set; }
public MainWindowViewModel()
{
script = new RelayCommand(o => MainButtonClick());
}
private void MainButtonClick()
{
Program start = new Program();
start.Begin();
}
}
You can bind the IsChecked of the CheckBox to a property in the ViewModel. Something like this should work:
<CheckBox Content="Option" IsChecked="{Binding ShowBrowser}" />
public bool ShowBrowser {get; set;}
You can then use the ShowBrowser property in your MainButtonClick method
Or you could use a Command Parameter as dymanoid pointed out in the comments. Like so:
<CheckBox Name="ShowBrowser" Content="Option" IsChecked="True" />
<Button Height="20"
Content="Run Script"
Command="{Binding Script }"
CommandParameter="{Binding ElementName=ShowBrowser, Path=IsChecked}
/>
And then your Method would look like this:
private void MainButtonClick(bool showBrowser)
{
Program start = new Program();
start.Begin();
}
This is of course assuming your RelayCommand class can handle parameters
I'm working on a WPF application using the MVVM pattern and I'm still fairly new to .NET development. My understanding is that the View should set its data context to a ViewModel and then any data related processing should be done in the ViewModel while the UI part should be handled in the view (XAML or code behind).
So I have a menu with each menu item bound to a DelegateCommand (using Prism) declared and handled in the ViewModel with keyboard shortcuts and it works flawlessly. However, I wanted to bind a menu item to a command in the View's code behind file as it doesn't manupulate any data (it just shows or hide a panel).
View (XAML)
<Window x:Class="Editor.Views.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:local="clr-namespace:Editor.Views"
xmlns:vm="clr-namespace:Editor.ViewModels"
mc:Ignorable="d"
x:Name="RootWindow"
WindowStartupLocation="CenterScreen"
Width="1200" Height="650">
<!-- Data Context -->
<Window.DataContext>
<vm:MainViewModel />
</Window.DataContext>
<!-- Keyboard Shortcuts -->
<Window.InputBindings>
<KeyBinding Modifiers="Control" Key="L" Command="{Binding ElementName=RootWindow, Path=ToggleLayersCommand}" />
</Window.InputBindings>
<!-- Main Menu -->
<Menu>
<MenuItem Header="View" Padding="5, 2">
<MenuItem Header="Toggle Layers Panel" InputGestureText="CTRL + L" Command="{Binding ElementName=RootWindow, Path=ToggleLayersCommand}" />
</MenuItem>
</Menu>
</Window>
View (Code behind)
public partial class MainWindow : Window
{
public DelegateCommand ToggleLayersCommand { get; private set; }
public MainWindow()
{
InitializeComponent();
ToggleLayersCommand = new DelegateCommand(ToggleLayersCommand_OnExecuted, () => true);
}
private void ToggleLayersCommand_OnExecuted()
{
LayerListPanel.Visibility = (LayerListPanel.Visibility == Visibility.Collapsed) ? Visibility.Visible : Visibility.Collapsed;
}
}
I named the window in XAML to find the command in the View instead of the ViewModel when binding the Command attribute. It seems to find it since I'm getting intellisense but it never fires.
I could use a click event instead even though I'd rather use a command but then how to bind the keyboard shortcut to the event?
I would define the command inside the ViewModel and have it change a property public Visibility LayerListPanelVisibility (which should also be defined in the ViewModel). Then I would bind LayerListPanel.Visibility to this property.
Keep your code-behind as empty as possible.
The reason that the command is not found by your Binding is that ToggleLayersCommand is null when the Binding is resolved. It is only shortly after binding resolution that you assign the proper command to ToggleLayersCommand. However, the Binding will not be updated as your Viewmodel does not raise a PropertyChanged event.
If you want to keep your command in the View, you can either raise a PropertyChanged event when you have assigned the command or you assign the command before you call InitializeComponent:
public MainWindow()
{
ToggleLayersCommand = new DelegateCommand(ToggleLayersCommand_OnExecuted, () => true);
InitializeComponent();
}
I had same problem with binding command to keybinding. All I have done is give to a button its own name and from view code behind set command to yours.
My example looks like so:
<KeyBinding x:Name="ShowDetailsKeyBinding"
Key="D"
Modifiers="Control" />
Code behind:
ShowDetailsKeyBinding.Command = new DelegateCommand(ShowDetailsOperation);
I use this solution to run my animations, other way you should create command in yours ViewModel.
You have to use the ViewModel for command and not the View:
ViewModel:
public partial class MainViewModel
{
public DelegateCommand ToggleLayersCommand { get; private set; }
public MainViewModel()
{
ToggleLayersCommand = new DelegateCommand(ToggleLayersCommand_OnExecuted, () => true);
}
private void ToggleLayersCommand_OnExecuted()
{
LayerListPanel.Visibility = (LayerListPanel.Visibility == Visibility.Collapsed) ? Visibility.Visible : Visibility.Collapsed;
//THIS WILL PROBABLY NOT WORK...
//You can use another public property to change your visibility.
// Create a public visibility and bind it to the correct item
}
}
XAML:
<Menu>
<MenuItem Header="View" Padding="5, 2">
<MenuItem Header="Toggle Layers Panel" InputGestureText="CTRL + L" Command="{Binding Path=ToggleLayersCommand}" />
</MenuItem>
</Menu>
I have a button
<Button Command="{Binding MyCommand}" />
But MyCommand is being hit when I long-press the button as well as when I just click it. Is there any way around this?
thanks
What you could do is the following:
Reference the System.Windows.Interactivity dll.
Define the namespace in your xaml code:
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Then in your xaml code wire up this event trigger:
<i:Interaction.Triggers>
<i:EventTrigger EventName="Tap" SourceName="btnTest">
<i:InvokeCommandAction Command="{Binding DoSomething}" />
</i:EventTrigger>
</i:Interaction.Triggers>
The EventName in this case is "Tap", the SourceName is the x:Name of the button you want to watch. Like this:
<Button Content="Click me" x:Name="btnTest"/>
Then in your ViewModel, you can wire it up to an ICommand, I typically use a RelayCommand:
private ICommand _DoSomething;
public ICommand DoSomething
{
get
{
if (_DoSomething == null)
{
_DoSomething = new RelayCommand(DoSomethingExecute);
}
return _DoSomething;
}
}
private void DoSomethingExecute()
{
Dispatcher.BeginInvoke(() =>
{
MessageBox.Show("btnTest on the tap event");
});
}
I tested, only the tap event is captured, not the long press event.
I'm trying to put together a very simple example app, but it is not working as intented.
Here is the scenario:
Caliburn.Micro, MVVM, Silverlight 5.0 - simple Conductor example from https://caliburnmicro.codeplex.com/wikipage?title=Screens%2c%20Conductors%20and%20Composition&referringTitle=Documentation (Simple Navigation)
I just put together a live example:
https://db.tt/kTIjKvRx
-> hit enter in textbox (messagebox displays 1x)
-> go to master and go back to login
-> hit enter in textbox (messagebox displays 2x!)
ShellViewModel
public class ShellViewModel : Conductor<object> {
public ShellViewModel() {
ShowLogin();
}
public void ShowLogin() {
ActivateItem(new LoginViewModel());
}
public void ShowMaster() {
ActivateItem(new MasterViewModel());
}
}
EDIT:
Same results with this:
public class ShellViewModel : Conductor<object> {
public ShellViewModel() {
LoginModel = new LoginViewModel();
MasterModel = new MasterViewModel();
ShowLogin();
}
public LoginViewModel LoginModel { get; set; }
public MasterViewModel MasterModel { get; set; }
public void ShowLogin() {
ActiveItem = LoginViewModel;
}
public void ShowMaster() {
ActiveItem = MasterViewModel;
}
}
ShellView
<UserControl
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"
mc:Ignorable="d" x:Class="Hele.ShellView"
d:DesignWidth="438" d:DesignHeight="200">
<Grid>
<StackPanel Orientation="Vertical" HorizontalAlignment="Left" VerticalAlignment="Top">
<Button x:Name="ShowLogin" Width="100" Height="30" Content="Login"/>
<Button x:Name="ShowMaster" Width="100" Height="30" Content="Master"/>
<ContentControl x:Name="ActiveItem" " />
</Grid>
</UserControl>
LoginView
<UserControl ... >
<Grid x:Name="LayoutRoot">
<StackPanel>
<TextBlock>Login</TextBlock>
<TextBox x:Name="Message" Text="{Binding Message, Mode=TwoWay}" >
<i:Interaction.Triggers>
<iex:KeyTrigger Key="Enter">
<cal:ActionMessage MethodName="Login" />
</iex:KeyTrigger>
</i:Interaction.Triggers>
</TextBox>
</StackPanel>
</Grid>
</UserControl>
LoginViewModel
public class LoginViewModel : Screen
{
public string Message { get; set; }
public void Login()
{
MessageBox.Show("login messagebox");
}
}
MasterView and MasterViewModel are just empty, nothing interesting there.
The above example just works fine, after clicking on Login button shows login view, on Master shows master view.
In the Login View there is a textbox which has an event trigger. After hitting Enter key, it calls a method from viewmodel and displays a messagebox.
The problem:
When going to master view and going back to login end hitting Enter - it shows the messagebox twice!
Going to master and again back -> it will display it 3x.. and so on.
I think the Trigger should fire only once. How can we achieve this behavior?
I think it's because your doing the ActivateItem each time your loading the view which rebinds the event handlers to the view. Try setting the ActiveItem property instead (which the ContentControl with x:Name="ActiveItem" is bound to). Also try using public variables to hold your view models:
public class ShellViewModel : Conductor<object> {
public ShellViewModel() {
ShowLogin();
}
public LoginViewModel { get; set; }
public MasterViewModel { get; set; }
public void ShowLogin() {
ActiveItem = LoginViewModel;
}
public void ShowMaster() {
ActiveItem = MasterViewModel;
}
}
EDIT
I was able to reproduce this and it seems to be an issue with the Expression Interactions. If I use a regular EventTrigger attached to key down it works fine:
<TextBox Width="50" Text="{Binding Message, Mode=TwoWay}" >
<i:Interaction.Triggers>
<!--<iex:KeyTrigger Key="Enter">
<cm:ActionMessage MethodName="Page1KeyPress" />
</iex:KeyTrigger>-->
<i:EventTrigger EventName="KeyDown">
<cm:ActionMessage MethodName="Page1KeyPress" >
<cm:Parameter Value="$source" />
<cm:Parameter Value="$eventArgs" />
</cm:ActionMessage>
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>
public void Page1KeyPress(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
MessageBox.Show("Page 1 Key Press");
}
Grid example with the trigger:
<Grid x:Name="LayoutRoot" DataContext="{Binding ProjectGrid, Source={StaticResource Locator}}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<GalaSoft_MvvmLight_Command:EventToCommand Command="{Binding LoadedCommand, Mode=OneWay}" PassEventArgsToCommand="True"/>
</i:EventTrigger>
</i:Interaction.Triggers>
In my ViewModel I set the LoadedCommand like this:
public RelayCommand<RoutedEventArgs> LoadedCommand {get;private set;}
And in the ViewModel initializer I have this:
public ProjectGridViewModel()
{
LoadedCommand = new RelayCommand<RoutedEventArgs>(e =>
{
this.DoLoaded(e);
}
);
}
Then, in my DoLoaded I'm trying to do this:
Grid _projectGrid = null;
public void DoLoaded(RoutedEventArgs e)
{
_projectGrid = e.OriginalSource as Grid;
}
You can see I'm trying to get rid of my Loaded="" in my Grid in my view, and do a RelayCommand instead. The issue is the OriginalSource brings back nothing. My loaded event is running nice this way, but I need to get the Grid in via the RoutedEventArgs it seems.
I tried passing in the Grid in the EventCommand with CommandParameter="{Binding ElementName=LayoutRoot}", but this just crashes VS2010 when hitting F5 and running the project.
Any ideas? Or a better way to do this? I had the Loaded event run in the views C# then call the ViewModel in the Views code-behind, but I'd like to do a nicer binding. Talking to the ViewMode in the Views code-behind feels like a hack.
You could try to bind the CommandParameter of the EventToCommand:
<GalaSoft_MvvmLight_Command:EventToCommand Command="{Binding LoadedCommand, Mode=OneWay}" CommandParameter="{Binding ElementName=LayoutRoot}" PassEventArgsToCommand="True"/>
Then, your code will be:
public RelayCommand<UIElement> LoadedCommand {get;private set;}
public ProjectGridViewModel()
{
LoadedCommand = new RelayCommand<UIElement>(e =>
{
this.DoLoaded(e);
}
);
}
Grid _projectGrid = null;
public void DoLoaded(UIElement e)
{
_projectGrid = e;
}
It should work fine :)
Bye.