Binding Command to ListView Items MVVM proper way - c#

I have an application list which is bound to a ListView control.
private List<_Application> _applicationList;
public List<_Application> applicationList
{
get { return _applicationList; }
set
{
_applicationList = value;
OnPropertyChanged();
}
}
The ListView ItemTemplate is set as a button.
<ListView ItemsSource="{Binding applicationList}"
BorderThickness="5"
Style="{DynamicResource ListViewStyle}">
<ListView.ItemTemplate>
<DataTemplate>
<Button Command="{Binding RunCommand}"
Style="{StaticResource ApplicationButtonStyle}"
Content="{Binding name}"
Background="{Binding colorRGB}" >
</Button>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
When I click on the button I want an application to be executed. My Model _Application contains an ActionCommand that runs the process.
public class _Application
{
public ActionCommand RunCommand
{
get { return new ActionCommand(action => Run()); }
}
private void Run()
{
Process p = new Process();
p.StartInfo.FileName = path;
try
{
p.Start();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
public _Application()
{
}
}
I am not sure, is it correct keeping the ActionCommand in the Model?
How could I correctly implement this in the MVVM pattern?
Where should the ActionCommand be placed and how to bind it to the ListView of Buttons so the correct _Application will be run?

I think the best way is pass the model (_Application) like a parameter to the command.
RunCommand = new RelayCommand(param => this.OnRun(param));
Command action
private void OnRun(_Application app)
{
//Any action with your model
}
Xaml
Command="{Binding DataContext.RunCommand, ElementName=PageRootKey}"
CommandParameter="{Binding Mode=OneWay}">

First of all you should use ICommand and not ActionCommand from one simple reason, if in the future you will want to replace the ActionCommand in something better that implements ICommand you won`t need to replace so many places in the code.
public ICommand RunCommand
{
get
{ return new ActionCommand(Run); }
}
The correct _Application will run as each item in the list view is connected to single _Application item in the collection.
Note: in above code I wrote ...ActionCommand(Run); since ActionCommand accepts an Action parameter you can write the code shortly and more readable like this.
I assume of course in the complete code _Application has properties of name and colorRgb.
As a matter of fact, if you wish to use correct MVVM pattern then colorRgb should not be in the viewmodel or the model. It is a view term. You should use a converter (read IValueConverter) to set a different color for each button (although it is not UX friendly).
And one last thing, property like name should be Name (capital N) as property names in C# should always begin with capital letters.

Related

MVVM WPF: Why is the property in the Model always null even after updating the text in textbox while running the application?

I gave up on MVP pattern and gave MVVM and WPF a try. I managed so far to navigate the menu showing different UserControls by clicking different buttons. DataBinding and Commands are working fine for navigation however i ran into a problem when DataBinding a textbox.
The property always get the value null and i do not know why. I added a command to get the UserID from a textbox when clicking a button and to show a MessageBox containing the UserID just to make it simple before i try anything else.
The MessageBox shows but it is empty. I added a Task.Run in the UsersModel.cs so i can see if the string updates on propertychange but value is null.
I am using a class for ObservableObject to notify OnPropertyChanged. And a RelayCommand class that inherits from ICommand. Both seems to work as intended but only when clicking the buttons to show different UserControls and Close the application. Below is sample code from View, Model and ViewModel. What am i missing?
RegisterMenu.xaml
<TextBox x:Name="RegTextUserID" Margin="163,66,148,112"
Style="{StaticResource ResourceKey=UserBox}"
Text="{Binding UserID, UpdateSourceTrigger=PropertyChanged}">
<TextBox.DataContext>
<models:UsersModel/>
</TextBox.DataContext>
</TextBox>
<Button x:Name="Button_Register" Content="Register" HorizontalAlignment="Left"
Margin="183,129,0,0" VerticalAlignment="Top"
Command="{Binding Path=GetUserCommand}">
<Button.DataContext>
<viewmodels:RegisterViewModel/>
</Button.DataContext>
</Button>
UsersModel.cs
public class UsersModel : ObservableObject
{
// backing fields
private string _userID;
// Properties
public string UserID // UserID is null
{
get { return _userID; }
set
{
SetProperty(ref _userID, value);
}
}
// testing loop for propertychange trigger
public UsersModel()
{
Task.Run(() =>
{
while (true)
{
Debug.WriteLine($"UserID: {UserID}");
Thread.Sleep(1000);
}
});
}
}
RegisterViewModel.cs
public class RegisterViewModel : ObservableObject
{
public RelayCommand GetUserCommand { get; set; }
public RegisterViewModel()
{
GetUserCommand = new RelayCommand(o =>
{
// Command is responding to button click.
});
}
}
EDIT 1
Updated the code in UsersModel.cs were i changed ObservableObject.cs to use [CallerMemberName] and SetProperty. I also removed the GetUserCommand code from RegisterViewModel.cs so i can only test the property in UsersModel.cs.
From what i understand is that the textBox binding is not working. I do not get any value even after using SetProperty method.
EDIT 2
To clarify, i used the following in XAML earlier (One DataContext) at the start. But i still had the same problem. The UsersModel property is not getting the text i type in the textbox.
<UserControl.DataContext>
<models: UserModel/>
</UserControl.DataContext>
I created a sample project with one textbox to try out setting a specific DataContext for the textbox like the code below. It is working in the sample. The output prints the text i write in the textbox. Why is it not working in the UsersModel.cs above?
MainWindow.xaml
<TextBox HorizontalAlignment="Left" Margin="249,60,0,0"
Text=" {Binding SomeText, UpdateSourceTrigger=PropertyChanged,
Mode=TwoWay}"
TextWrapping="Wrap" VerticalAlignment="Top" Width="120">
<TextBox.DataContext>
<local:TestModel/>
</TextBox.DataContext>
</TextBox>
TestModel.cs (this works, i can see the loop output everytime i change the text in the textbox).
class TestModel
{
private string _someText;
public string SomeText
{
get { return _someText; }
set { _someText = value; }
}
public TestModel()
{
Task.Run(() =>
{
while (true)
{
Debug.WriteLine(SomeText);
Thread.Sleep(1000);
}
});
}
}
I was looking for the error in the wrong place. I managed to locate the error to the Style. I've tested to remove the style and the binding worked fine. I have to adjust the code of the Themes i am using. Thanks for all the comments.

Is it possible to bind the MenuItem.IsEnabled property to a different Context?

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.

How to tell which Button has been clicked, when it's generated dynamically? (MVVM)

I have a SearchResultsViewModel with observable collection of recipe class and a command to show a recipe:
private ObservableCollection<Recipe> _searchedRecipes;
public ObservableCollection<Recipe> SearchedRecipes
{
get
{
return _searchedRecipes;
}
set
{
_searchedRecipes = value;
OnPropertyChanged();
}
}
#endregion
#region Show Recipe Command
public ICommand ShowRecipeCommand { get { return new RelayCommand(() =>
ExecuteShowRecipeCommand()); } }
public void ExecuteShowRecipeCommand()
{
_locator.Main.CurrentViewModel = new DisplayRecipeViewModel();
}
#endregion
Another ViewModel performs a query and passes results in the constructor of this ViewModel.
In XAML part of the SearchResultsViewModel, results are presented as Buttons dynamically. Each Recipe is a Button with it's name as content:
<StackPanel>
<ItemsControl ItemsSource="{Binding Path = SearchedRecipes}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Path=Name}" Command="{Binding ShowRecipeCommand}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
I want ShowRecipeCommand to create new DisplayRecipeViewModel with a View bound to it, displaying the properties of Recipe that was clicked but I don't know how
to tell which Button was clicked.
Is it possible to do this without code behind ??
You could just move the command property to the Recipe class. Then each Button (or rather each data object that is represented by a Button) has its own command and you always know which one that was clicked.
If the Recipe class is auto-generated by some ORM such as for example Entity Framework, you could create another partial class where you define the command property.

WPF Button Command not firing, what am I missing?

I feel bad posting this because I see a ton of similar posts, but after going through them all I can't quite diagnose my issue still. What I have is a WPF app designed with the MVVM pattern and using a RelayCommand() implementation for commands.
In my user control's XAML I set the data context here :
<UserControl.DataContext>
<viewModel:SidePanelViewModel />
</UserControl.DataContext>
Then further down in the XAML I have this snippet where I assign a button's command
<TextBlock FontWeight="Bold" Margin="0,0,0,10">Service List</TextBlock>
<ListBox MaxHeight="100"
ItemsSource="{Binding ServiceList}"
SelectedItem="{Binding ServiceToRemove}">
</ListBox>
<Button HorizontalAlignment="Left" Width="60" Margin="0,10"
Command="{Binding RemoveServiceCommand}">Remove</Button>
I am binding the button to the Command RemoveApplicationCommand which I define in the SidePanelViewModel here :
public ICommand RemoveServiceCommand
{
get { return new RelayCommand(RemoveService, CanRemoveService); }
}
private void RemoveService()
{
ServerList.Remove(ServiceToRemove);
}
private bool CanRemoveService()
{
return true;
}
The problem
If I debug, the getter for RemoveServiceCommand will be reached when the button starts up, but when I click the button the code doesn't reach it. I had a very similar implementation (or so I think) working before, so this is really puzzling me. How can I get the command to fire on click?
Command="{Binding RemoveApplicationCommand}"
Did you mean RemoveServiceCommand?
Turns out the debugger was going over RemoveService the entire time but I had not put a breakpoint there. I had a wrong name in my RemoveService implementation ServerList.Remove() should have been ServiceList.Remove(). I assumed the debugger would hit a breakpoint in the RemoveServiceCommand property's getter but it turns out it doesn't hit that when you click the button.
You're returning a new RelayCommand in your getting, but not saving / caching the instance. Save it in a member variable.
if (_cmd == null)
_cmd = new ....
return _cmd;
Try implementing like this
private ICommand finishCommand;
public ICommand FinishCommand
{
get
{
if (this.finishCommand == null)
{
this.finishCommand = new RelayCommand<object>(this.ExecuteFinishCommand, this.CanExecutFinishCommand);
}
return this.finishCommand;
}
}
private void ExecuteFinishCommand(object obj)
{
}
private bool CanExecutFinishCommand(object obj)
{
return true;
}

Can/Should I create instances of a UserControl in my viewmodel?

Question regarding UserControls and MVVM. I have my wpf app with a main View/ViewModel. The viewmodel has a ObservableCollection of my usercontrol(s) that a listbox is bound to. The UserControl instances are added to the collection at run time based on events.
My question is if it's within the MVVM pattern to create the usercontrol objects from my main viewmodel? Example below in the onSomeEvent method is the code I'm unsure of, if this is where I should handle it? This doesn't feel right to me, but I'm still wrapping my mind around mvvm. Should I be adding the user control viewmodel here instead? Thanks for any guidance.
private ObservableCollection<string> myList = new ObservableCollection<string>();
public ObservableCollection<string> MyList
{
get { return myList; }
set
{
myList = value;
RaisePropertyChangedEvent("MyList");
}
}
public void onSomeEvent(string someData1, string someData2)
{
this.MyList.Add(new Views.MyUserControl(someData1, someData2));
}
Ok, I mocked up some code based on feedback from BradleyDotNET and dymanoid as I wrap my mind around it. Pasting it here to see if I'm on the right track.
I modified the listbox in my mainview xaml to add a template:
<ListBox Name="lbMain" Margin="10" ItemsSource="{Binding MyList, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock Text="{Binding Test1}" FontWeight="Bold" />
<TextBlock Text="{Binding Test2}" FontWeight="Bold" />
</WrapPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I then created a simple class like this to populate a few fields.
public class MyData
{
public MyData(string test1, string test2)
{
this.Test1 = test1;
this.Test2 = test2;
}
private string test1;
public string Test1
{
get
{
return test1;
}
set
{
test1 = value;
}
}
private string test2;
public string Test2
{
get
{
return test2;
}
set
{
test2 = value;
}
}
}
Then in my mainviewmodel I did this:
public void onSomeEvent(string someData1, string someData2)
{
this.MyList.Add(new MyData(someData1, someData2));
}
No, your viewmodel should not create any UserControl instances, because they are views. Furthermore, your main viewmodel shouldn't contain any collections of any views. As #BradleyDotNET mentioned, DataTemplate is the right way for it.
You should change your main viewmodel collection. It shouldn't contain any UserControls (views), but rather their viewmodels. Assuming that you have defined DataTemplates for your sub-viewmodels in XAML, you will get your views automagically created by WPF.
This could look like:
<DataTemplate DataType = "{x:Type local:UserControlViewModel}">
<local:UserControl/>
</DataTemplate>
With this approach, WPF sets the DataContext property value to the sub-viewmodel instance automatically, so you can easily define your bindings in that UserControl.

Categories

Resources