Context Menu Keep Getting Wrong Object using Mvvm - c#

I am making a winodws 8 phone application and trying to have a context menu on it from the Windows Phone tool kit.
I been following this tutorial but instead of list box I am using a long list selector that is built into WP8
<DataTemplate x:Key="GroceryListItemTemplate">
<StackPanel Grid.Column="1" Grid.Row="1">
<TextBlock x:Name="tbName" TextWrapping="Wrap" Text="{Binding Name}" FontSize="32"/>
<TextBlock x:Name="tbProductInfo" TextWrapping="Wrap" Text="{Binding ProductInfoLabel}" HorizontalAlignment="Left"/>
</StackPanel>
<toolkit:ContextMenuService.ContextMenu>
<toolkit:ContextMenu>
<toolkit:MenuItem Header="Edit"
Command="{Binding GroceryItemsVm.EditGroceryItemCmd, Source={StaticResource Locator}}"
CommandParameter="{Binding}"/>
<toolkit:MenuItem Header="Delete" Command="{Binding GroceryItemsVm.DeleteGroceryItemCmd, Source={StaticResource Locator}}"
CommandParameter="{Binding}"/>
</toolkit:ContextMenu>
</toolkit:ContextMenuService.ContextMenu>
</DataTemplate>
above is a stripped down of what my code looks like
here is my list selector
<phone:LongListSelector IsGroupingEnabled="True" ItemsSource="{Binding GroceryItems}" HideEmptyGroups="True" LayoutMode="List" Grid.Row="1">
<phone:LongListSelector.ItemTemplate>
<StaticResource ResourceKey="GroceryListItemTemplate"/>
</phone:LongListSelector.ItemTemplate>
</phone:LongListSelector>
Here is my mvvm code I have
public class GroceryItemsVm : ViewModelBase
{
public GroceryItemsVm()
{
if (IsInDesignMode)
{
}
else
{
EditGroceryItemCmd = new RelayCommand<GroceryItem>(this.Edit);
DeleteGroceryItemCmd = new RelayCommand<GroceryItem>(this.Delete);
GroceryItems = // method that gets all items back as grouped.
}
}
private List<Group<GroceryItem>> groceryItems = null;
/// <summary>
/// Sets and gets the GroceryItems property.
/// Changes to that property's value raise the PropertyChanged event.
/// </summary>
public List<Group<GroceryItem>> GroceryItems
{
get
{
return groceryItems;
}
set
{
if (groceryItems == value)
{
return;
}
RaisePropertyChanging(() => GroceryItems);
groceryItems = value;
RaisePropertyChanged(() => GroceryItems);
}
}
private async void Delete(GroceryItem obj)
{
// trigged on context delete
}
private void Edit(GroceryItem obj)
{
// triggered on context edit
}
public RelayCommand<GroceryItem> EditGroceryItemCmd
{
get;
private set;
}
public RelayCommand<GroceryItem> DeleteGroceryItemCmd
{
get;
private set;
}
}
public class GroceryItem : ObservableObject
{
/// <summary>
/// The <see cref="Name" /> property's name.
/// </summary>
public const string NamePropertyName = "Name";
private string name = "";
/// <summary>
/// Sets and gets the Name property.
/// Changes to that property's value raise the PropertyChanged event.
/// </summary>
public string Name
{
get
{
return name;
}
set
{
if (name == value)
{
return;
}
RaisePropertyChanging(() => Name);
name = value;
RaisePropertyChanged(() => Name);
}
}
}
Now when I run it, It works for the first time, whatever item I choose to edit it, will get the right object for it. However the next object will always be the same. It never changes it choice after the selection is done.
Edit
Here is an example.
https://onedrive.live.com/redir?resid=FAE864D71B4770C6!19080&authkey=!ACUC2xXmZLVD7fE&ithint=file%2c.zip
Run it
Trigger Context Menu to show over "1"
Hit Edit - note dialog message (will say 1)
Hit "Go back button"
Trigger Context Menu to show over "3"
Hit Edit - note dialog message (will say 3)
The only thing I can think of is override the back button for the pages I am going to and just do a Navigate to the page. It is kinda stupid but that's all I can think off.
public partial class MvvmView1 : PhoneApplicationPage
{
// Constructor
public MvvmView1()
{
InitializeComponent();
// Sample code to localize the ApplicationBar
//BuildLocalizedApplicationBar();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
NavigationService.GoBack();
}
protected override void OnBackKeyPress(CancelEventArgs e)
{
NavigationService.Navigate(new Uri("/MainPage.xaml", UriKind.Relative));
}
}

This is common problem with ContextMenu. I have been trying for some time to think of a solution searching all around for something. You said after you click once it nevers gets it right.
Try the following:
Add the unloaded handler to you contextmenu as follows:
<DataTemplate x:Key="GroceryListItemTemplate">
<StackPanel Grid.Column="1" Grid.Row="1">
<TextBlock x:Name="tbName" TextWrapping="Wrap" Text="{Binding Name}" FontSize="32"/>
<TextBlock x:Name="tbProductInfo" TextWrapping="Wrap" Text="{Binding ProductInfoLabel}" HorizontalAlignment="Left"/>
</StackPanel>
<toolkit:ContextMenuService.ContextMenu>
<toolkit:ContextMenu ***Unloaded="ContextMenu_Unloaded"***>
<toolkit:MenuItem Header="Edit"
Command="{Binding GroceryItemsVm.EditGroceryItemCmd, Source={StaticResource Locator}}"
CommandParameter="{Binding}"/>
<toolkit:MenuItem Header="Delete" Command="{Binding GroceryItemsVm.DeleteGroceryItemCmd, Source={StaticResource Locator}}"
CommandParameter="{Binding}"/>
</toolkit:ContextMenu>
</toolkit:ContextMenuService.ContextMenu>
</DataTemplate>
Remove the * I added them to emphasize the changes.
And then the code behind for that handler would be:
private void ContextMenu_Unloaded(object sender, RoutedEventArgs e)
{
var conmen = (sender as ContextMenu);
if (conmen != null)
conmen.ClearValue(DataContextProperty);
}
Let me know if this works.

Based on the comments, you have a GroceryItemsVm class that looks something like the following.
public class GroceryItemVm : INotifyPropertyChanged
{
public string Name { get; set; }
public string ProductInfoLabel{ get; set; }
public ICommand EditGroceryItemCmd { get; private set; }
public ICommand DeleteGroceryItemCmd { get; private set; }
}
And so the GroceryItems property that your LLS is binding to would be
public IEnumerable<GroceryItemVm> GroceryItems { get; set;}
If this is the case, then the DataContext of the items within your DataTemplate is an instance of GroceryItemsVm. All of your bindings within the DataTemplate should bind directly to that instance
<DataTemplate x:Key="GroceryListItemTemplate">
<StackPanel Grid.Column="1" Grid.Row="1">
<TextBlock x:Name="tbName" TextWrapping="Wrap" Text="{Binding Name}" FontSize="32"/>
<TextBlock x:Name="tbProductInfo" TextWrapping="Wrap" Text="{Binding ProductInfoLabel}" HorizontalAlignment="Left"/>
</StackPanel>
<toolkit:ContextMenuService.ContextMenu>
<toolkit:ContextMenu>
<toolkit:MenuItem Header="Edit" Command="{Binding EditGroceryItemCmd}"/>
<toolkit:MenuItem Header="Delete" Command="{Binding DeleteGroceryItemCmd}"/>
</toolkit:ContextMenu>
</toolkit:ContextMenuService.ContextMenu>
</DataTemplate>

Related

WPF .NET unable to add new instance to ObservableCollection

I am new to WPF and losing my mind with issues. I have a view, viewmodel and model. I want the user user to fill in some information in the view, press button to confirm and then have a new instance of the model (with the user specified parameters) added to the ObservableCollection and to my local database.
View: (unrelated stuff hidden)
<TextBox DataContext="{DynamicResource RiderequestViewModel}" Margin="15,0,15,0" Grid.Column="1" Grid.Row="1" FontSize="12" Height="25" Text="{Binding Riderequest.Time}"/>
<TextBox DataContext="{DynamicResource RiderequestViewModel}" Margin="15,0,15,0" Grid.Column="1" Grid.Row="2" FontSize="12" Height="25" Text="{Binding Riderequest.LocationFrom}"/>
<TextBox DataContext="{DynamicResource RiderequestViewModel}" Margin="15,0,15,0" Grid.Column="1" Grid.Row="3" FontSize="12" Height="25" Text="{Binding Riderequest.LocationTo}"/>
<Button DataContext="{DynamicResource RiderequestViewModel}" x:Name="nextBtn" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="5" Content="Verder" Width="150" Foreground="White" Command="{Binding AddRiderequestCommand}" Click="NextBtn_Click"/>
ViewModel RiderequestViewModel:
namespace Drink_n_Drive.ViewModel
{
class RiderequestViewModel: BaseViewModel
{
private Riderequest riderequest;
private ObservableCollection<Riderequest> riderequests;
public ObservableCollection<Riderequest> Riderequests
{
get
{
return riderequests;
}
set
{
riderequests= value;
NotifyPropertyChanged();
}
}
public Riderequest Riderequest
{
get
{
return riderequest;
}
set
{
riderequest= value;
NotifyPropertyChanged();
}
}
public ICommand AddRiderequestCommand { get; set; }
public ICommand ChangeRiderequestCommand { get; set; }
public ICommand DeleteRiderequestCommand { get; set; }
public RiderequestViewModel()
{
LoadRiderequests(); //load existing from DB
LinkCommands(); //Link ICommands with BaseCommands
}
private void LoadRiderequests()
{
RiderequestDataService riderequestDS = new RiderequestDataService();
Riderequests= new ObservableCollection<Riderequests>(riderequestDS .GetRiderequests());
}
private void LinkCommands()
{
AddRiderequestCommand = new BaseCommand(Add);
ChangeRiderequestCommand = new BaseCommand(Update);
DeleteRiderequestCommand = new BaseCommand(Delete);
}
private void Add()
{
RiderequestDataService riderequestDS = new RitaanvraagDataService();
riderequestDS.InsertRiderequest(riderequest); //add single (new) instance to the DB
LoadRiderequests(); //Reload ObservableCollection from DB
}
private void Update()
{
if (SelectedItem != null)
{
RiderequestDataService riderequestDS = new RiderequestDataService();
riderequestDS.UpdateRiderequest(SelectedItem);
LoadRiderequests(); //refresh
}
}
private void Delete()
{
if (SelectedItem != null)
{
RiderequestDataService riderequestDS = new RiderequestDataService();
riderequestDS.DeleteRiderequest(SelectedItem);
LoadRiderequests();
}
}
private Riderequest selectedItem;
public Riderequest SelectedItem
{
get { return selectedItem; }
set
{
selectedItem = value;
NotifyPropertyChanged();
}
}
}
}
Pressing the button simply does nothing and I don't know why. I also have a diffrent page where I want to show a datagrid of all instances in the ObservableCollection like this:
<DataGrid Grid.Row="1" Grid.ColumnSpan="2" Grid.RowSpan="3" DataContext="{DynamicResource RitaanvragenViewModel}" ItemsSource="{Binding Ritaanvragen}" SelectedItem="{Binding SelectedItem}" />
But the grid just shows completly empty. I have added some dummydata to my DB but still doesn't work.
My appologies for the mix of English and Dutch in the code.
I'm not 100% sure about it but i would do something like this:
As for first step I would change the TextBox to look like this:
<TextBox DataContext="{DynamicResource Ritaanvraag}" Margin="15,0,15,0" Grid.Column="1" Grid.Row="1" FontSize="12" Height="25" Text="{Binding Path=Time, Mode=OneWayToSource}"/>
There's no need to pass your ViewModel to it as a DataSource because your View's first few meta-data related lines should already define what ViewModel does it belong to.
When you not specify the type of your binding, it will use a default binding type which depends on the current object. You're using a TextBox now so it will have a TwoWay binding by default.
If you only want to accept data from the user and you don't want to show the data if your model has any then you should use OneWayToSource. (Note: OneWay is a direction between source -> view.)
I would also remove the DataSource from your DataGrid because you already set it's ItemSource:
<DataGrid Grid.Row="1" Grid.ColumnSpan="2" Grid.RowSpan="3" ItemsSource="{Binding Ritaanvragen}" SelectedItem="{Binding SelectedItem}" />

Some Issue with Two-way Binding - Not working as expected in UWP

Problem: When location is changed via ComboBox cb1 the related location TextBlock does not change to updated value.
I am self learning and below is experiment code on binding that has
public EmpDeptViewModel vm; its initialize on button click event as below
private void btn2_Click(object sender, RoutedEventArgs e) {
vm = new EmpDeptViewModel();
this.Bindings.Update(); }
The XAML looks like this.
<ListView x:Name="listview3" ItemsSource="{x:Bind vm.InstanceOfDepartmentData}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="classes:Department">
<StackPanel Orientation="Horizontal">
<TextBlock Margin="5">
<Run Text="DeptNo: " /><Run Text="{x:Bind DeptNo}" />
</TextBlock>
<TextBlock Margin="5">
<Run Text="DeptName: " /><Run Text="{x:Bind DeptName}" />
</TextBlock>
<TextBlock Margin="5">
<Run Text="Location: " /><Run Text="{x:Bind Location, Mode=OneWay}" />
</TextBlock>
<ComboBox x:Name="cb1" ItemsSource="{Binding Source={StaticResource MyLocatonList}, Path=ListofLocationsInsideViewModel, Mode=TwoWay}" DisplayMemberPath="LocationName" SelectedValuePath="LocationName" SelectedValue="{x:Bind Location}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Problem: When location is changed via ComboBox cb1 the related location TextBlock does not change to updated value.
The x:DataType="classes:Department" looks like this.
public class Department : BindableBase
{ private string _location;
public Department(int pdeptNo, string pdeptName, string plocation)
{
DeptNo = pdeptNo;
DeptName = pdeptName;
Location = plocation;
ListOfDeparmentEmployees = new List<Employee>(); }
public int DeptNo { get; set; }
public string DeptName { get; set; }
public string Location {
get { return this._location; }
set { this.SetProperty(ref this._location, value); }
}
public List<Employee> ListOfDeparmentEmployees { get; set; }
}
You may be bound in the wrong position
In your ComboBox, you set TwoWay to the ItemsSource. This does not make sense. You cannot change the Location if you modify the value of the ComboBox.
Try this:
Xaml
...
<ComboBox x:Name="cb1" ItemsSource="{Binding Source={StaticResource MyLocatonList}, Path=ListofLocationsInsideViewModel}"
DisplayMemberPath="LocationName" SelectedValuePath="LocationName" SelectedValue="{x:Bind Location,Mode=TwoWay}" />
...
However, if you write it directly, it will cause an endless loop and then report an error. You need to rewrite the Location property of the Department class.
Department.cs
...
public string Location
{
get { return this._location; }
set
{
if (_location != value)
{
this.SetProperty(ref this._location, value);
}
}
}
...
In addition, please note whether your BindableBase base class implements the INotifyPropertyChanged interface, which is the basis for modifying the UI while modifying the data.
Best regards.

ComboBox Selected Item not updating

Problem
I am trying to bind a ComboBox's SelectedItem to a custom class but this does not update when the property is changed.INotifyPropertyChanged is implemented.
The DataContext
The DataContext is a custom class which contains many properties, but an extract of this is below. You can see it implements INotifyPropertyChanged and this called when the two properties are changed.
public class BctsChange : INotifyPropertyChanged
{
#region declarations
private byContact _Engineer;
public byContact Engineer
{
get { return _Engineer; }
set
{
_Engineer = value;
NotifyPropertyChanged("Engineer");
OnEngineerChanged();
}
}
private BctsSvc.DOSets _LeadingSet;
public BctsSvc.DOSets LeadingSet
{
get { return _LeadingSet; }
set { _LeadingSet = value; NotifyPropertyChanged("LeadingSet"); }
}
#endregion
#region INotify
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
public BctsChange()
{
Engineer = new byContact(Environment.UserName);
}
private void OnEngineerChanged()
{
if (Engineer != null)
{
BctsSvc.DOSets leadSet = GetLeadingSetFromDeptCode(Engineer.DeptCode);
if (leadSet == null) return;
LeadingSet = leadSet;
}
}
private static BctsSvc.DOSets GetLeadingSetFromDeptCode(string DeptCode)
{
BctsSvc.BctsServiceSoapClient svc = new BctsSvc.BctsServiceSoapClient();
BctsSvc.DOSets setX = svc.GetSetFromDeptCode(DeptCode);
return setX;
}
}
The Window XAML
I have several controls on the window, but to keep the code simple I believe the following extract will suffice.
<Window x:Class="MyNamespace.wdSubmit"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my="clr-namespace:MyNamespace"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
x:Name="ucReqForm"
Title="wdSubmit" >
<StackPanel Orientation="Vertical" HorizontalAlignment="Stretch">
<GroupBox Header="Engineer Details" Name="grpOwnerDetails" >
<StackPanel Orientation="Vertical">
<Grid VerticalAlignment="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="35"/>
</Grid.ColumnDefinitions>
<Label Content="{Binding Engineer.FullName, FallbackValue='Please select an engineer by clicking →', Mode=OneWay}" Margin="5,0" IsEnabled="True" FontStyle="Italic" />
<Button Content="{StaticResource icoSearch}" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Grid.Column="1" Height="23" Name="btnSelectEngineer" Margin="0,0,5,0" HorizontalAlignment="Stretch" ToolTip="Search for an engineer responsible" Click="btnSelectEngineer_Click" />
</Grid>
<ComboBox Height="23" x:Name="ddSet2" Margin="5,0" ItemsSource="{Binding LeadingSets, Mode=OneWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}" SelectedItem="{Binding LeadingSet, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,NotifyOnTargetUpdated=True}" >
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding SetName}" ToolTip="{Binding HelpInfo}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<my:LabelledDropdown Height="23" x:Name="ddSet" Margin="5,0" ItemsSource="{Binding LeadingSets, Mode=OneWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}" SelectedItem="{Binding LeadingSet, Mode=TwoWay,NotifyOnTargetUpdated=True,NotifyOnSourceUpdated=True}" Label="e.g. BodyHardware">
<my:LabelledDropdown.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding SetName}" ToolTip="{Binding HelpInfo}"/>
</DataTemplate>
</my:LabelledDropdown.ItemTemplate>
</my:LabelledDropdown>
</StackPanel>
</GroupBox>
</StackPanel>
</Window>
The above extract contains:
A Label that contains a contact's name, and a button to search for a contact, bound to the FullName of the Engineer
A ComboBox that contains departments within the company, bound to an ObservableCollection<DOSets>, which contains a list of departments
Two ComboBoxes, one which is a custom one and the other which is temporary to ensure the bug is not within the control. These are Databound to LeadingSet
Window Code Behind
In the code behind I set the DataContext to CurrentChange. When the user wants to select a different Engineer then this will update the selected department for the engineer in CurrentChange.
When the user changes the engineer, the data binding for the engineer is updated, but the selected department (Leading Set) isn't.
//Usings here
namespace MyNamespace
{
public partial class wdSubmit : Window, INotifyPropertyChanged
{
private BctsSvc.BctsServiceSoapClient svc;
private BctsChange _CurrentChange;
public BctsChange CurrentChange
{
get { return _CurrentChange; }
set { _CurrentChange = value; OnPropertyChanged("CurrentChange"); }
}
private List<BctsSvc.DOSets> _LeadingSets;
public List<BctsSvc.DOSets> LeadingSets
{
get
{
return _LeadingSets;
}
}
public wdSubmit()
{
InitializeComponent();
svc = new BctsSvc.BctsServiceSoapClient();
_LeadingSets = svc.GetLeadSets().ToList();
OnPropertyChanged("LeadingSets");
this._CurrentChange = new BctsChange();
this.DataContext = CurrentChange;
CurrentChange.PropertyChanged += new PropertyChangedEventHandler(CurrentChange_PropertyChanged);
}
void CurrentChange_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
OnPropertyChanged("CurrentChange");
OnPropertyChanged(e.PropertyName);
}
private void btnSelectEngineer_Click(object sender, RoutedEventArgs e)
{
byContact newContact = new frmSearchEngineer().ShowSearch();
if (newContact != null)
{
CurrentChange.Engineer = newContact;
PropertyChanged(CurrentChange, new PropertyChangedEventArgs("LeadingSet"));
PropertyChanged(CurrentChange.LeadingSet, new PropertyChangedEventArgs("LeadingSet"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(CurrentChange, new PropertyChangedEventArgs(propertyName));
}
}
}
I've realised the problem may be due to the LeadingSet, returned when the engineer is changed, being a different instance to that in the ObservableCollection.

How to know which element is tapped in ListView? [duplicate]

This question already has answers here:
How to get clicked item in ListView
(5 answers)
Closed 7 years ago.
I've got a ListView with a DataTemplate like this, using MVVM pattern
<ListView ItemsSource="{Binding Source}"
IsItemClickEnabled="True"
commands:ItemsClickCommand.Command="{Binding ItemClickedCommand}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding A}" />
<Button Content="{Binding B}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
ItemsClickCommand is defined in this way
public static class ItemsClickCommand
{
public static readonly DependencyProperty CommandProperty = DependencyProperty.RegisterAttached("Command", typeof(BindableCommand), typeof(ItemsClickCommand), new PropertyMetadata(null, OnCommandPropertyChanged));
public static void SetCommand(DependencyObject d, BindableCommand value)
{
d.SetValue(CommandProperty, value);
}
public static BindableCommand GetCommand(DependencyObject d)
{
return (BindableCommand)d.GetValue(CommandProperty);
}
private static void OnCommandPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var control = d as ListViewBase;
if (control != null)
control.ItemClick += OnItemClick;
}
private static void OnItemClick(object sender, ItemClickEventArgs e)
{
var control = sender as ListViewBase;
var command = GetCommand(control);
if (command != null && command.CanExecute(e.OriginalSource))
command.ExecuteWithMoreParameters(e.OriginalSource, e.ClickedItem);
}
}
What I'm asking is how can I know if user tap on the TextBlock or Button.
I tried to handle ItemClickCommand event in this way in ViewModel to search controls in VisualTree (is this the best solution?), but the cast to DependencyObject doesn't work (returns always null)
public void ItemClicked(object originalSource, object clickedItem)
{
var source = originalSourceas DependencyObject;
if (source == null)
return;
}
There are a few solutions that come to mind
Solution 1
<ListView
x:Name="parent"
ItemsSource="{Binding Source}"
IsItemClickEnabled="True"
Margin="20">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding A}" />
<Button
Content="{Binding B}"
Command="{Binding DataContext.BCommand, ElementName=parent}"
CommandParameter="{Binding}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Note how the ListView has the name set to "parent" with the attribute: x:Name="parent" and how the binding for the button's command uses that. Also note that the command will be provided with a parameter that is the reference to the data source for the element that was clicked.
The view model for this page will look like this:
public class MainViewModel : MvxViewModel
{
public ObservableCollection<MySource> Source { get; private set; }
public MvxCommand<MySource> BCommand { get; private set; }
public MainViewModel()
{
Source = new ObservableCollection<MySource>()
{
new MySource("e1", "b1"),
new MySource("e2", "b2"),
new MySource("e3", "b3"),
};
BCommand = new MvxCommand<MySource>(ExecuteBCommand);
}
private void ExecuteBCommand(MySource source)
{
Debug.WriteLine("ExecuteBCommand. Source: A={0}, B={1}", source.A, source.B);
}
}
'MvxCommand' is just a particular implementation of ICommand. I used MvvMCross for my sample code but you don't have to do that - you can use whatever MvvM implementation you need.
This solution is appropriate if the responsibility to handle the command lies with the view model for the page that contains the list.
Solution 2
Handling the command in the view model for the page that contains the list may not always be appropriate. You may want to move that logic in code that is closer to the element that is being clicked. In that case, isolate the data template for the element in its own user control, create a view model class that corresponds to the logic behind that user control and implement the command in that view model. Here is how the code would look like:
The XAML for the ListView:
<ListView
ItemsSource="{Binding Source}"
IsItemClickEnabled="True"
Margin="20">
<ListView.ItemTemplate>
<DataTemplate>
<uc:MyElement DataContext="{Binding Converter={StaticResource MySourceToMyElementViewModelConverter}}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
The XAML for the user control representing one element:
<Grid>
<StackPanel>
<TextBlock Text="{Binding Source.A}" />
<Button Content="{Binding Source.B}" Command="{Binding BCommand}" />
</StackPanel>
</Grid>
The source code for MySourceToMyElementViewModelConverter:
public class MySourceToMyElementViewModelConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
return new MyElementViewModel((MySource)value);
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotSupportedException();
}
}
The view model for the main page:
public class MainViewModel : MvxViewModel
{
public ObservableCollection<MySource> Source { get; private set; }
public MainViewModel()
{
Source = new ObservableCollection<MySource>()
{
new MySource("e1", "b1"),
new MySource("e2", "b2"),
new MySource("e3", "b3"),
};
}
}
The view model for the user control representing one element in the list:
public class MyElementViewModel : MvxViewModel
{
public MySource Source { get; private set; }
public MvxCommand BCommand { get; private set; }
public MyElementViewModel(MySource source)
{
Source = source;
BCommand = new MvxCommand(ExecuteBCommand);
}
private void ExecuteBCommand()
{
Debug.WriteLine("ExecuteBCommand. Source: A={0}, B={1}", Source.A, Source.B);
}
}
Solution 3
Your sample assumes that the view model for the main page exposes a list of data model elements. Something like this:
public ObservableCollection<MySource> Source { get; private set; }
The view model for the main page could be changed so that it exposes a list of view model elements instead. Something like this:
public ObservableCollection<MyElementViewModel> ElementViewModelList { get; private set; }
Each element in ElementViewModelList would correspond to an element in Source. This solution can get slightly complex if the contents of Source changes at run time. The view model of the main page will need to observe Source and change ElementViewModelList accordingly. Going further don this path you may want to abstract the concept of a collection mapper (something similar with an ICollectionView) and provide some generic code for doing so.
For this solution, the XAML will look like this:
<ListView
ItemsSource="{Binding ElementViewModelList}"
IsItemClickEnabled="True"
Margin="20">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding A}" />
<Button Content="{Binding B}" Command="{Binding BCommand}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Notes for Solution 1, 2 and 3
I see that your original sample associates a commanding not with the button inside of the element but with the entire element. That raises the question: what are you going to do with the inner button? Will you have a situation where the user can click either on the element or on the inner button? That may not be the best solution as far as UI/UX goes. Be mindful of that. Just as an exercise and in order to get closer to your original sample, here is what you can do if you want to associate commanding with the entire element.
Wrap your entire element in a button with a custom style. That style will modify the way a click is handled visually. The simplest form of that is to have the click not create any visual effect. This change applied to Solution 1 (it can easily be applied to Solution 2 and Solution 3 as well) would look something like this:
<ListView
x:Name="parent"
ItemsSource="{Binding Source}"
IsItemClickEnabled="True"
Margin="20">
<ListView.ItemTemplate>
<DataTemplate>
<Button
Command="{Binding DataContext.BCommand, ElementName=parent}"
CommandParameter="{Binding}"
Style="{StaticResource NoVisualEffectButtonStyle}">
<StackPanel>
<TextBlock Text="{Binding A}" />
<TextBlock Text="{Binding B}" />
</StackPanel>
</Button>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
In this case you would have to write NoVisualEffectButtonStyle but that is a simple task. You would also need to decide what kind of commanding you want to associate with the inner button (otherwise why would you have an inner button). Or, more likely you could transform the inner button in something like a textbox.
Solution 4
Use Behaviors.
First, add a reference to "Behaviors SDK".. Then modify your XAML code:
...
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:core="using:Microsoft.Xaml.Interactions.Core"
...
<Grid>
<ListView ItemsSource="{Binding Source}" IsItemClickEnabled="True" Margin="20">
<interactivity:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="ItemClick">
<core:InvokeCommandAction
Command="{Binding BCommand}"
InputConverter="{StaticResource ItemClickedToMySourceConverter}" />
</core:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding A}" />
<TextBlock Text="{Binding B}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
ItemClickedToMySourceConverter is just a normal value converter:
public class ItemClickedToMySourceConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
return (MySource)(((ItemClickEventArgs)value).ClickedItem);
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotSupportedException();
}
}
The view model will look like this:
public class Main4ViewModel : MvxViewModel
{
public ObservableCollection<MySource> Source { get; private set; }
public MvxCommand<MySource> BCommand { get; private set; }
public Main4ViewModel()
{
Source = new ObservableCollection<MySource>()
{
new MySource("e1", "b1"),
new MySource("e2", "b2"),
new MySource("e3", "b3"),
};
BCommand = new MvxCommand<MySource>(ExecuteBCommand);
}
private void ExecuteBCommand(MySource source)
{
Debug.WriteLine("ExecuteBCommand. Source: A={0}, B={1}", source.A, source.B);
}
}

Force or otherwise initiate TwoWay TestBox data binding

I've got a WPF TextBox with TwoWay binding to a ViewModel property. I also have a ToolBar with a Button. When the Button is clicked, it executes a command on the same ViewModel that will do something with the property the TextBox is bound to.
Unfortunately it looks like the Binding only sends the text back to the binding target when the TextBox loses focus. The Button on the Toolbar however does not take focus when clicked. The upshot being that when the Command executes it does not have the text from the textbox, but rather the last value that was bound.
The Xaml looks like so:
<DockPanel LastChildFill="True" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" >
<ToolBarTray Background="White" DockPanel.Dock="Top">
<ToolBar Band="1" BandIndex="1">
<Button Command="{Binding QueryCommand}">
<Image Source="images\media_play_green.png" />
</Button>
</ToolBar>
</ToolBarTray>
<DataGrid VerticalAlignment="Top" DockPanel.Dock="Top" Height="450" AutoGenerateColumns="True"
ItemsSource="{Binding}" DataContext="{Binding Results}" DataContextChanged="DataGrid_DataContextChanged"/>
<TextBox DockPanel.Dock="Bottom" Text="{Binding Sql, Mode=TwoWay}"
AcceptsReturn="True" AcceptsTab="True" AutoWordSelection="True" TextWrapping="WrapWithOverflow"/>
</DockPanel>
How do I get the TextBox's Text binding to update the ViewModel when the ToolBar button is pressed. There is nothing fancy going on in the ViewModel which looks like so:
public class MainViewModel : ViewModelBase
{
private readonly IMusicDatabase _database;
/// <summary>
/// Initializes a new instance of the MainViewModel class.
/// </summary>
public MainViewModel(IMusicDatabase database)
{
_database = database;
QueryCommand = new RelayCommand(Query);
}
public RelayCommand QueryCommand { get; private set; }
private async Task QueryAndSetResults()
{
Results = await _database.Query(Sql);
}
private void Query()
{
QueryAndSetResults();
}
private IEnumerable<object> _results;
public IEnumerable<object> Results
{
get
{
return _results;
}
private set
{
Set<IEnumerable<object>>("Results", ref _results, value);
}
}
private string _sql = "SELECT * FROM this WHERE JoinedComposers = 'Traditional'";
public string Sql
{
get { return _sql; }
set
{
Set<string>("Sql", ref _sql, value);
}
}
}
You can use the UpdateSourceTrigger property of the binding, setting it to PropertyChanged makes the TextBox refresh the binding every time the text changes, not just when losing focus:
<TextBox DockPanel.Dock="Bottom"
Text="{Binding Sql, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
AcceptsReturn="True"
AcceptsTab="True"
AutoWordSelection="True"
TextWrapping="WrapWithOverflow"/>
More info at MSDN.

Categories