在MvvmCross中为UITableView单元格内按钮绑定命令的正确方法
Hey there! Let's break down how to get this working properly in MvvmCross, starting with a quick look at your proposed approaches, then landing on the cleanest, most MVVM-aligned solution.
First, let's evaluate your four ideas:
Idea 1: Expose button outlet and bind in
GetOrCreateCellFor
This works, but it shoves binding logic into theTableViewSource—breaking MVVM's separation of concerns. View-layer code shouldn't handle binding setup, which makes maintenance harder as your app scales. Not recommended.Idea 2: Pass
ShowItemDetailsCommandto each cell and bind
This is the best MVVM-compliant approach! It leverages MvvmCross's binding system to keep logic in the ViewModel, where it belongs. We'll build out this approach below.Idea 3: Simple callback from cell to view
This falls back to traditional iOS delegate patterns, ditching the benefits of MvvmCross's binding. It creates tight coupling between your cell and view, making testing and refactoring harder. Skip this.Idea 4: Individual ViewModels per cell
Overkill for this use case. Your cells only need to display data and trigger a navigation action—no complex independent logic. Adding a ViewModel per cell would add unnecessary complexity.
Step-by-Step Implementation
Let's modify your code to implement the recommended approach (Idea 2, refined):
1. Update the ItemsViewModel to use a parameterized command
We need a command that accepts an Item parameter to know which detail page to navigate to:
public class Item { public string Name { get; set; } } public class ItemsViewModel : MvxViewModel { public List<Item> Items { get; } // Change to MvxCommand<Item> to accept the selected item public MvxCommand<Item> ShowItemDetailsCommand { get; } readonly IMvxNavigationService _navigationService; readonly IDatabaseService _databaseService; public ItemsViewModel(IMvxNavigationService navigationService, IDatabaseService databaseService) { ShowItemDetailsCommand = new MvxCommand<Item>(ShowItemDetails); _navigationService = navigationService; _databaseService = databaseService; Items = _databaseService.SelectItems(); } // Now accepts the Item parameter directly async void ShowItemDetails(Item item) { await _navigationService.Navigate<ItemDetailsViewModel, Item>(item); } }
2. Update the ItemsView to configure cell bindings
We'll set up the binding for the cell's button directly in the view's binding set, keeping the cell decoupled from the parent ViewModel:
public partial class ItemsView : MvxTableViewController<ItemsViewModel> { public ItemsView() : base("ItemsView", null) {} public override void ViewDidLoad() { base.ViewDidLoad(); TableView = View as UITableView; var source = new MvxTableViewSource(TableView) { ItemTemplateSelector = new MvxDefaultTemplateSelector(ItemCellView.kCellId) }; var bindings = this.CreateBindingSet<ItemsView, ItemsViewModel>(); // Bind the items collection to the table source bindings.Bind(source).To(vm => vm.Items); // Configure bindings for each cell's controls bindings.Bind(source) .For(s => s.ItemTemplateBindings) .To(vm => new { // Bind the name label to the Item's Name property Name = new { Path = "Name" }, // Bind the button's command to the ViewModel's command, passing the current Item as parameter DetailButton = new { Command = vm.ShowItemDetailsCommand, CommandParameter = "$dataContext" // "$dataContext" refers to the current Item in the cell } }); bindings.Apply(); TableView.Source = source; TableView.ReloadData(); } public class TableViewSource : MvxTableViewSource { public TableViewSource(UITableView tableView) : base(tableView) { TableView.RegisterNibForCellReuse(UINib.FromName("ItemCellView", NSBundle.MainBundle), ItemCellView.kCellId); } protected override UITableViewCell GetOrCreateCellFor(UITableView tableView, NSIndexPath indexPath, object item) { return TableView.DequeueReusableCell(ItemCellView.kCellId, indexPath) as ItemCellView; } } }
3. Simplify the ItemCellView
Since we're handling all bindings in the parent view, we can keep the cell clean (or retain cell-internal bindings if you prefer):
public partial class ItemCellView : MvxTableViewCell { public const string kCellId = "item_cell"; // [Outlet] UIButton DetailButton exists in the .designer.cs file public ItemCellView(IntPtr handle) : base(handle) { // Optional: If you prefer cell-internal bindings, uncomment this block // this.DelayBind(() => // { // var itemsViewModel = (this.ViewController as ItemsView).ViewModel; // var bindings = this.CreateBindingSet<ItemCellView, Item>(); // bindings.Bind(Name).To(i => i.Name); // bindings.Bind(DetailButton) // .For(b => b.Command) // .To(() => itemsViewModel.ShowItemDetailsCommand); // bindings.Bind(DetailButton) // .For(b => b.CommandParameter) // .To(i => i); // bindings.Apply(); // }); } }
Key Notes
- Using
MvxCommand<Item>directly solves your original question of how to pass the selected item to the navigation logic. - The
$dataContextbinding path refers to the current item bound to the cell, ensuring we pass the correct item to the command. - This approach keeps all business logic (navigation) in the ViewModel, making it testable and separated from UI code—true to MVVM principles.
内容的提问来源于stack exchange,提问作者Ivan Mir

