I'm attempting to implement a theme-selection menu in a WPF/MVVM application. I've got the selection itself working, but can't seem to figure out how to set IsChecked on the appropriate MenuItem with pure databinding (aka without breaking the MVVM pattern).
XAML:
<MenuItem Header="_Theme">
<MenuItem Header="Classic" Command="{Binding ChangeThemeCommand}" CommandParameter="Classic" />
<MenuItem Header="Metro White" Command="{Binding ChangeThemeCommand}" CommandParameter="MetroWhite" />
</MenuItem>
ViewModel:
RelayCommand _changeThemeCommand;
public ICommand ChangeThemeCommand
{
get
{
return _changeThemeCommand ?? (_changeThemeCommand = new RelayCommand(param =>
{
ThemeManager.CurrentTheme = param.ToString();
}));
}
}
The theming is being handled by Actipro's WPF control suite (http://www.actiprosoftware.com); as you can see, the current theme is represented as a string only.
My problem lies in figuring out how to bind IsChecked in a way that will mark the MenuItem for the active theme. The way the XAML is currently structured, that would mean matching the current theme name to the MenuItem's CommandParameter.
Any tips/pointers would be greatly appreciated.
Your first problem is that you are hard-coding all your themes. Better would be to create a class called Theme:
public class Theme : INotifyPropertyChanged
{
public string Name { get; set; } // Implement PropertyChanged event on this.
public bool Checked { get; set; } // Implement PropertyChanged event on this.
}
In your main view model, have an observable collection of these, then fill it up with your themes, i.e.:
ObservableCollection<Theme> Themes { get; private set; }
In constructor, something like:
Themes.Add(new Theme() { Name = "Classic" });
Themes.Add(new Theme() { Name = "MetroWhite" });
Now your context menu should look something like:
<MenuItem Header="_Theme" ItemsSource="{Binding Themes}">
<MenuItem.ItemTemplate>
<DataTemplate>
<MenuItem Header="{Binding Name}" IsChecked="{Binding Checked}" IsCheckable="True"/>
</DataTemplate>
</MenuItem.ItemTemplate>
</MenuItem>
Now, this gives you a set of themes, and when you click on one it's Checked property is set. Now you can assign your Command to the MenuItems, preferably as part of the Theme class (i.e. Theme.Set() seems like a reasonable OO design to me). Should all be pretty straightforward from here on.
Update
How do I enforce that only one theme is selected at once?
Assuming you have a MainViewModel, extend the Theme constructor to take a reference back to the MainViewModel. Then in your SetTheme() command, iterate over all other themes making sure they are not Checked.
void SetTheme()
{
foreach (Theme theme in MainViewModel.Themes)
{
if (theme != this)
{
theme.Checked = false;
}
}
// Do actual theme setting .
}
Why should I implement INotifyPropertyChanged?
Because the above doesn't work if you dont. Sure, you could just implement it for Checked, but as a matter of good practice I recommend implementing it for all public accessible properties that form part of the interface. That way if you use this ViewModel with some different View later on that wants to edit these properties, everything will just work.
Related
I have a ListView bound to a collection of objects (called Users, in this case), and the template includes a ContextActions menu. One of the menu items needs to be enabled or disabled depending on a condition having nothing directly to do with the items in the view (whether or not there's a Bluetooth connection to a certain kind of peripheral). What I'm doing right now is iterating the Cells in the TemplatedItems property and setting IsEnabled on each.
Here's the XAML for the ListView, stripped down to the parts that matter for my question:
<ListView ItemsSource="{Binding .}" ItemTapped="item_Tap">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Label}">
<TextCell.ContextActions>
<MenuItem
Text="Copy to other device"
ClassId="copyMenuItem"
Clicked="copyMenuItem_Click" />
</TextCell.ContextActions>
</TextCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Here's how I'm setting the property values now:
foreach (Cell cell in usersListView.TemplatedItems)
{
foreach (MenuItem item in cell.ContextActions)
{
if ("copyMenuItem" == item.ClassId)
{
item.IsEnabled = isBluetoothConnected;
}
}
}
That works, but i don't like it. It's obviously out of line with the whole idea of data-bound views. I'd much rather have a boolean value that I can bind to the IsEnabled property, but it doesn't make sense from an object design point of view to add that to the User object; it has nothing to do with what that class is about (representing login accounts). I thought of wrapping User in some local class that exists just to tape this boolean property onto it, but that feels strange also since the value will always be the same for every item in the collection. Is there some other way to bind the MenuItem.IsEnabled property?
Use relative binding
Get ready in your view model class, inherit INotifyPropertyChanged or your BaseViewModel.
public class YourViewModel : INotifyPropertyChanged
{
private string isBluetoothConnected;
public string IsBluetoothConnected
{
get => isBluetoothConnected;
set => SetProperty(ref isBluetoothConnected, value);
}
public ObservableCollection<User> Users { get; private set; }
}
Add a name to ListView for reference, and apply relative binding in MenuItem.
<ListView
x:Name="UserListView"
ItemsSource="{Binding Users}"
ItemTapped="item_Tap">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Label}">
<TextCell.ContextActions>
<MenuItem
IsEnabled="{Binding Path=BindingContext.IsBluetoothConnected, Source={x:Reference UserListView}}"
Text="Copy to other device"
ClassId="copyMenuItem"
Clicked="copyMenuItem_Click" />
</TextCell.ContextActions>
</TextCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
It turns out that this case of BindableProperty is, in fact, not bindable: https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/menuitem#enable-or-disable-a-menuitem-at-runtime
One must add a Command property to the MenuItem and assign a BindingContext to that, and set its executability. Here's the latest version of my code, which does work:
<MenuItem
Text="Copy to other device"
Clicked="copyMenuItem_Click"
BindingContext="{x:Reference usersListView}"
Command="{Binding BindingContext.CopyCommand}" />
public class UsersViewModel
{
public Command CopyCommand { get; set; }
public bool IsBluetoothConnected
{
get { return isBluetoothConnected; }
set
{
isBluetoothConnected = value;
if (CopyCommand.CanExecute(null) != value)
{
CopyCommand.ChangeCanExecute();
}
}
}
public ObservableCollection<User> Users { get; private set; }
private bool isBluetoothConnected;
public async System.Threading.Tasks.Task<int> Populate( )
{
CopyCommand = new Command(( ) => { return; }, ( ) => IsBluetoothConnected); // execute parameter is a no-op since I really just want the canExecute parameter
IList<User> users = await App.DB.GetUsersAsync();
Users = new ObservableCollection<User>(users.OrderBy(user => user.Username));
return Users.Count;
}
}
I'm still not entirely happy with this; it contaminates the view model with the concerns of a specific view. I'm going to see if I can separate the Command from the view model. But it does accomplish my primary goal, bringing this UI implementation into the data binding paradigm.
Using MVVM
I am trying to pass data entered in a control (a textbox in the attached code) in one view (view1) and use that data in the second view (view2). At the moment, by declaring all my views in the App.xaml file, I can bind the textblock in view2 with the information entered in the textbox in view1 and see it displayed in the said textblock. But I want to use the information entered in view2's view model as well but dont know how to access it there to use the information.
Can somebody tell me how to go about doing this? Thanks!
App.xaml [declaration of resources]
<Application.Resources>
<vws:DefaultVM x:Key="DefaultVMApp"></vws:DefaultVM>
<vws:View1 x:Key="View1App"></vws:View1>
<vws:View2 x:Key="View2App"></vws:View2>
<vm:AppVM x:Key="AppVMApp"></vm:AppVM>
<vm:View1VM x:Key="View1VMApp"></vm:View1VM>
<vm:View2VM x:Key="View2VMApp"></vm:View2VM>
</Application.Resources>
View1.xaml
<UserControl.DataContext>
<StaticResource ResourceKey="View1VMApp"></StaticResource>
</UserControl.DataContext>
<Grid Background="Aqua">
<StackPanel Margin="100">
<TextBox x:Name="firstNameTextBoxView1" Text="{Binding View1InfoClass.FirstName, Mode=OneWayToSource, UpdateSourceTrigger=PropertyChanged}"></TextBox>
<Button Command="{Binding Source={StaticResource AppVMApp}, Path=View2ButtonCommand}" Content="Go to view2" Height="20" Width="70" />
</StackPanel>
</Grid>
View2.xaml
<UserControl.DataContext>
<StaticResource ResourceKey="View2VMApp"></StaticResource>
</UserControl.DataContext>
<Grid Background="Beige">
<StackPanel Margin="100">
<TextBlock x:Name="View1TextBlock" Text="{Binding Source={StaticResource View1VMApp}, Path=View1InfoClass.FirstName}" ></TextBlock>
</StackPanel>
</Grid>
AppVM
public class AppVM : ObservableObject
{
//Create a property that controls current view
private static object _currentView = new DefaultVM();
public object CurrentView
{
get { return _currentView; }
private set
{
OnPropertyChanged(ref _currentView, value);
}
}
private string _textboxText;
public string TextboxText
{
get { return _textboxText; }
set
{
OnPropertyChanged(ref _textboxText, value);
}
}
public AppVM()
{
View1ButtonCommand = new RelayCommand(ShowView1, AlwaysTrueCommand);
View2ButtonCommand = new RelayCommand(ShowView2, AlwaysTrueCommand);
DefaultCommand = new RelayCommand(ShowDefault, AlwaysTrueCommand);
}
//Instantiate the relaycommands, we will need to instantiate relaycommand objects for every command we need to perform.
//This means that we will need to do this for preses of all buttons
public RelayCommand View1ButtonCommand { get; private set; }
public RelayCommand View2ButtonCommand { get; private set; }
public RelayCommand DefaultCommand { get; private set; }
public void ShowDefault(object dummy)
{
CurrentView = new DefaultVM();
}
public void ShowView1(object dummy)
{
CurrentView = new View1();
}
public void ShowView2(object dummy)
{
CurrentView = new View2();
}
public bool AlwaysTrueCommand(object dummy)
{
return true;
}
}
The fundamental problem in your code is that you have dedicated a pre-defined view model object to each of the user controls. This is really bad. A user control's data context must be left alone, for the client code (e.g. your main window) to determine, and to use for binding to specific properties that the user control exposes.
Unfortunately, there's not enough context in your question to provide a clear, complete answer. But to fix your issue, you need to do things differently:
First and foremost, "decouple" the view models you are using for your user control from the user controls themselves. Do this by adding dependency properties to each user control, and then letting the main view where the user controls are used decide what to bind to each of those dependency properties. Do not allow the user controls themselves to set their own data contexts.
Having done that, you may find that you can just use the same view model for the two user controls as for the main view. I.e. you'll set the main view's data context to the single view model, the user controls will inherit that data context, and you'll bind, for example, the TextboxText property to the appropriate declared dependency property in each user control. This way, that single property will represent state for both user controls at the same time.
One hopes that will be enough to get you back on track. If not, consider searching Stack Overflow for other questions related to view models and their relationships to user controls. For example, these questions:
Issue with DependencyProperty binding
XAML binding not working on dependency property?
WPF DataBinding with MVVM and User Controls
Other questions which don't address your scenario exactly, but which should give you some ideas for alternative ways to structure your view model(s):
MVVM : Share data between ViewModels
Sharing non control related data between WPF ViewModel and UserControl
Sharing data between different ViewModels
Sharing state between ViewModels
I have an UserControl with a button inside. This button needs to add some items to a Grid that's inside said UC. I'm aware I can do this with a Click event.
The issue here is I am using MVVM and altering data outside their corresponding ViewModel would break the format (So to say).
Is there a way to create an ICommand Dependency Property so I can bind said DP to the button and have the functionality of adding the item to the Grid in my ViewModel? (I already have the List in both my UC and my ViewModel and they are working as expected)
Thank you.
Found a way to solve it in the way I was trying to. Leaving the answer here so people may use it:
1) In your User Control's code-behind, create a Dependency Property. I choose ICommand, since in my ViewModel I set it as a DelegateCommmand:
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register(
"Command",
typeof(ICommand),
typeof(UserControl));
public ICommand Command
{
get
{
return (ICommand)GetValue(CommandProperty);
}
set
{
SetValue(CommandProperty, value);
}
}
2) In your UserControl's XAML code, bind this Dependency Property (In this case, a button):
<Grid DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}}">
<Button Command="{Binding Command}" />
</Grid>
3) Next, on your ViewModel, declare a Command property and configure accordingly:
public ICommand ViewModelCommand { get; set; }
public ViewModelConstructor()
{
ViewModelCommand = new DelegateCommand(ViewModelCommandExecute);
}
private void ViewModelCommandExecute()
{
// Do something
}
4) Finally, on your View where the UserControl is nested, we declare the binding:
<UserControls:UserControl Command={Binding ViewModelCommand}/>
This way, the binding will take place and you can bind Commands from the buttons of any User Control to your ViewModels without breaking MVVM.
The basic way is to create an Object (ie MyCommand) which implements ICommand, and nest it inside your ViewModel. Inside MyCommand you have no access to your ViewModel. You can workaround it (ie pass a reference to the ViewModel in MyCommand constructor) but at the end it's too much code (for simple stuff like this). I think almost nobody really do this.
Most use a DelegateCommand which resolve (most of) the above issues.
Last but not least, just use event handlers.
If you code them simply like this:
void Grid_MouseMove(object sender, MouseEventArgs e)
{ viewModel.SaveMousePosition(e.GetPosition()); }
you are not breaking any MVVM rule.
And you can't handle the above event with Commands.
There is no Command for MouseMove (there is none for most events), and you can't pass event parameters in a Command.
You can handle every event using Interaction.Triggers like this
But you still miss the capability to handle event parameters (and add ugly XAML).
To me, until WPF will support databinding in event handlers, like
Grid MouseMove="{Binding SaveMousePosition(e)}"
code behind is still the most effective way to handle events.
I faced similar problem and this question/answers helped me the most; so I will post my solution here in case somebody else will google it later. Made with mvvm light.
I had a custom winforms control as a Model and a WPF control as a View. So, xaml of View (I have an usercontrol for my View, no app.xaml):
<UserControl.Resources>
<ResourceDictionary>
<viewModel:ViewModelLocator x:Key="Locator" />
</ResourceDictionary>
</UserControl.Resources>
<UserControl.DataContext>
<Binding Path = "Main" Source="{StaticResource Locator}"></Binding>
</UserControl.DataContext>
<Grid>
<Button Command="{Binding Zoom, ElementName=Wrapper}"></Button>
<viewModel:ProfileWrapper x:Name="Wrapper" >
</viewModel:ProfileWrapper>
</Grid>
Click of a Button is routed to a RelayCommand Zoom in ProfileWrapper (which is where my Model implemented)
Then the xaml of ProfileWrapper is straghtforward:
<Grid>
<WindowsFormsHost>
<local:ManualControl x:Name="abc" ></local:ManualControl>
</WindowsFormsHost>
</Grid>
And the codebehind of ProfileWrapper :
public partial class ProfileWrapper : UserControl
{
public ProfileWrapper()
{
InitializeComponent();
test = abc;
Command = new RelayCommand(() => test.bZoomIn());
}
public ManualControl test;
public RelayCommand Zoom { get; set; }
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register(
"Zoom",
typeof(ICommand),
typeof(ProfileWrapper));
public ICommand Command
{
get
{
return (ICommand)GetValue(CommandProperty);
}
set
{
SetValue(CommandProperty, value);
}
}
}
My MainViewModel class is empty and all fuctionality goes to ProfileWrapper class, which might be bad, but at least it works.
I know it's a generic title, but my question is specific. I think it will boil down to a question of practice. So, I have the following code:
public partial class MainWindow : Window
{
InitializeComponent();
MyViewModel viewModel = new MyViewModel();
this.myGrid.DataContext = viewModel;
}
public class MyViewModel
{
public ICommand SomeCommandProperty { get { return this.someCommandProperty; }}
}
public class ComponentCollection : Panel
{
public ComponentCollection()
{
for (int i = 0; i < n; i++)
{
this.Children.Add(new Component());
}
}
}
public class Component : UIElement
{
public Component()
{
this.InputBindings.Add(new MouseBinding(SomeCommandProperty, new MouseGesture(MouseAction.LeftClick)));
}
}
I could easily aggregate the ViewModel that owns SomeCommandProperty into the Component class, but I'm currently waiving that option assuming there is another way.
Component is a child of ComponentCollection which is child of a Grid which DataContext is MyViewModel. ComponentCollection as the name suggests contains a collection of Components.
<Grid Name="myGrid">
<someNamespace:ComponentCollection x:Name="componentCollection"/>
</Grid>
It's the same scenario as the XAML below, but with TextBlock. I guess I'm trying to replicate what's being done in the XAML below programatically. Again, Component's top most ancestor's DataContext is set to ViewModel.
<Grid Name="myGrid">
<TextBlock Text="SomeText">
<TextBlock.InputBindings>
<MouseBinding Command="{Binding SomeCommandProperty}" MouseAction="LeftClick" />
</TextBlock.InputBindings>
</TextBlock>
</Grid>
Update 1
Basically, I have a custom control which inherit from a Panel which children are a collection of Component. It's not a hack, like I've mentioned, I could directly have access to SomeCommandProperty If I aggregate the ViewModel into Component. Doing so, however, feels icky. That is, having direct access to ViewModel from a Model.
I guess the question I'm asking is. Given the situation that Component's parent UIElement's DataContext is set to MyViewModel, is it possible to access SomeCommandProperty without Component owning a reference to the MyViewModel that owns SomeCommandProperty? Programatically, that is.
Using ItemsControl doesn't change the fact that I still need to bind SomeCommandProperty to each Items.
Update 2
See code above.
Update 3
Apparently, there isn't a mechanism I know of that will set the binding on the Command property of an InputBinding.
For example if my Component class were to Inherit from ButtonBase instead of UIElement, I would have the Command property to which I could easily set the binding programatically using FrameWorkElement's SetBinding. Unfortunately, I can't do this with InputBinding's Command property.
public class Component : ButtonBase
{
public Component()
{
System.Windows.Data.Binding binding = new System.Windows.Data.Binding
{
RelativeSource = new System.Windows.Data.RelativeSource(System.Windows.Data.RelativeSourceMode.FindAncestor, typeof(ComponentCollection), 1 ),
Path = new PropertyPath("DataContext.SomeCommandProperty")
};
// I can do this.
this.SetBinding(this.CommandProperty, binding);
// But I want to do something like below. Note: It's a pseudo code.
MouseBinding mouseBinding = new MouseBinding();
mouseBinding.SetBinding(mouseBinding.CommandProperty, binding);
this.InputBindings.Add(mouseBinding);
}
}
Update 4
BindingOperations.SetBinding can be used on Objects that don't have direct access to SetBinding.
Solution
MouseBinding mouseBinding = new MouseBinding();
BindingOperations.SetBinding(mouseBinding, MouseBinding.CommandProperty, binding);
this.InputBindings.Add(mouseBinding);
Use an ItemsControl for this. Don't try to hack something together yourself when there is a built-in class that already does this.
You can also access the ViewModel from a parent UI element in the Visual Tree by using a RelativeSource binding:
<ItemsControl>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="SomeText">
<TextBlock.InputBindings>
<!-- See how I'm using RelativeSource to get a hold of the DataContext of the parent ItemsControl -->
<MouseBinding Command="{Binding DataContext.SomeCommandProperty,
RelativeSource={RelativeSource AncestorType=ItemsControl}}"
MouseAction="LeftClick" />
</TextBlock.InputBindings>
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
On my journey to learning MVVM I've established some basic understanding of WPF and the ViewModel pattern. I'm using the following abstraction when providing a list and am interested in a single selected item.
public ObservableCollection<OrderViewModel> Orders { get; private set; }
public ICollectionView OrdersView
{
get
{
if( _ordersView == null )
_ordersView = CollectionViewSource.GetDefaultView( Orders );
return _ordersView;
}
}
private ICollectionView _ordersView;
public OrderViewModel CurrentOrder
{
get { return OrdersView.CurrentItem as OrderViewModel; }
set { OrdersView.MoveCurrentTo( value ); }
}
I can then bind the OrdersView along with supporting sorting and filtering to a list in WPF:
<ListView ItemsSource="{Binding Path=OrdersView}"
IsSynchronizedWithCurrentItem="True">
This works really well for single selection views. But I'd like to also support multiple selections in the view and have the model bind to the list of selected items.
How would I bind the ListView.SelectedItems to a backer property on the ViewModel?
Add an IsSelected property to your child ViewModel (OrderViewModel in your case):
public bool IsSelected { get; set; }
Bind the selected property on the container to this (for ListBox in this case):
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding Mode=TwoWay, Path=IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
IsSelected is updated to match the corresponding field on the container.
You can get the selected children in the view model by doing the following:
public IEnumerable<OrderViewModel> SelectedOrders
{
get { return Orders.Where(o => o.IsSelected); }
}
I can assure you: SelectedItems is indeed bindable as a XAML CommandParameter
There is a simple solution to this common issue; to make it work you must follow ALL the following rules:
Following Ed Ball's suggestion, on your XAML command databinding, define the CommandParameter attribute BEFORE the Command attribute. This a very time-consuming bug.
Make sure your ICommand's CanExecute and Execute methods have a parameter of type object. This way you can prevent silenced cast exceptions that occur whenever the databinding's CommandParameter type does not match your Command method's parameter type:
private bool OnDeleteSelectedItemsCanExecute(object SelectedItems)
{
// Your code goes here
}
private bool OnDeleteSelectedItemsExecute(object SelectedItems)
{
// Your code goes here
}
For example, you can either send a ListView/ListBox's SelectedItems property to your ICommand methods or the ListView/ListBox itself. Great, isn't it?
I hope this prevents someone from spending the huge amount of time I did to figure out how to receive SelectedItems as a CanExecute parameter.
One can try creating an attached property.
Doing so will save one from adding the IsSelected property for each and every list you bind. I have done it for ListBox, but it can be modified for use a in a list view.
<ListBox SelectionMode="Multiple"
local:ListBoxMultipleSelection.SelectedItems="{Binding SelectedItems}" >
More info: WPF – Binding ListBox SelectedItems – Attached Property VS Style .
If you're using MVVM-LIGHT you can use this pattern:
https://galasoft.ch/posts/2010/05/handling-datagrid-selecteditems-in-an-mvvm-friendly-manner
Not especially elegant but looks like it should be reliable at least