How to change DataContext of a ContextMenu item? - xaml

In my XAML, I have a read-only ListView and on right click of a listView item I want to provide a click-option which will update the property of an object and eventually update 'My Name' column of the list view.
Let's say my listView is in Primary.Xaml. DataContext of this xaml is ObjectInfo.
Xaml has a listView and a ContextMenu like this:
<ListView x:Name="lview" SelectedIndex="0" Width="{Binding ElementName=gridItems, Path=ActualWidth}" Height="{Binding ElementName=gridItems, Path=ActualHeight}" Style="{DynamicResource ListViewStyle}">
<ListView.ContextMenu>
<ContextMenu>
<MenuItem x:Name="mitem" Click="mitem_Click" >
<MenuItem.Header>
<Label HorizontalContentAlignment="Left" Content="My Name"/>
</MenuItem.Header>
</MenuItem>
</ContextMenu>
</ListView.ContextMenu>
<ListView.View>
<GridView ColumnHeaderContainerStyle="{StaticResource GridViewColumnHeaderStyle}">
<GridViewColumn Header="Id#" Width="80" DisplayMemberBinding="{Binding Id}" />
<GridViewColumn Header="Number#" Width="100" DisplayMemberBinding="{Binding Number}" />
<GridViewColumn Header="My Name" Width="80" DisplayMemberBinding="{Binding MyName}" />
</GridView>
</ListView.View>
So, my listView's DataContext is ObjectInfo which has MyName as a read-only property.
public string MyName { get; private set; }
I'm loading the property like this:
MyName = dr.GetString("DifferentObject" + "MyName");
Which means I have to update the property on "DifferentObject" object and listen the change from Primary.Xaml.
This is the property from "DifferentObject" I want to update and bind into my ContextMenu.
private static readonly PropertyInfo<bool> IsMyNameProperty = RegisterValueProperty<bool>(x => x.IsMyName);
public bool IsMyName
{
get { return GetProperty(IsMyNameProperty); }
set
{
SetProperty(IsMyNameProperty, value);
if (value)
{
SetProperty(MyNameIdProperty, UserId);
SetProperty(MyNameProperty, Name);
}
else
{
SetProperty(MyNameIdProperty, 0);
SetProperty(MyNameProperty, string.Empty);
}
}
}
Code behind of my xaml, I'm planning to do all the logic and datacontext manipulation:
private void mitem_Click(object sender, RoutedEventArgs e)
{
var selectedItem = lview.SelectedItem as ObjectInfo;
var Name = GetInfo().Name;
if (selectedItem.MyName != Name)
{
var item = lview.DataContext as DifferentObject; //I have item as null
//after I get item, I will be able to assign true to IsMyName
}
}
So! My question is, how do I have a different DataContext for my ContextMenu?

Related

Why doesnt my image bind until I save my contentpage unless I save and reload

I'm currently trying to bind a few properties when I click a button and it pushes a new page.
Starting from the top, this is how my app is setup
public App()
{
InitializeComponent();
MainPage = new NavigationPage(new MainPage());
}
I have a "MainPage" which is essentially the first page that shows when starting the app.
In my MainPage.xaml
I've set the BindingContext to the MainViewModel
<ContentPage.BindingContext>
<viewModels:MainViewModel/>
</ContentPage.BindingContext>
And I also have a button which has it's Command bound to a Command in my MainViewModel
<Button Text="New Goal"
HeightRequest="50" WidthRequest="100"
TextColor="White"
Margin="10"
CornerRadius="4"
Command="{Binding NewGoalCommand}">
<Button.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#8FDF70" Offset="0.1" />
<GradientStop Color="#1DBE95" Offset="1.0" />
</LinearGradientBrush>
</Button.Background>
</Button>
The MainViewModel is pretty straightforward. It has a Command property which I initialize in the constructor
public ObservableCollection<Item> Items { get; set; } = new ObservableCollection<Item>();
public Command NewGoalCommand { get; set; }
public MainViewModel()
{
NewGoalCommand = new Command(() => ShowNewGoalPage());
}
private async void ShowNewGoalPage()
{
await Application.Current.MainPage.Navigation.PushAsync(new NewGoal(SelectedItem));
}
NewGoal.xaml
As you can see in the code it's invoking NewGoal which is my second page which shows up when I click the button, this page does show up when I click the button which to me, indicates that the binding was successful.
The same goes for this page, I'm setting the BindingContext to my other ViewModel which is responsible for it's corresponding view, like so
<ContentPage.BindingContext>
<viewModel:NewGoalViewModel/>
</ContentPage.BindingContext>
And I've also added components which are going to bind to it's corresponding property so that when I click "Save" it adds that item to the collection inside the MainViewModel
<ContentPage.Content>
<StackLayout Spacing="0">
<Image WidthRequest="50" HeightRequest="50"
Margin="10"
Source="{Binding ItemModel.ImageSource}"/>
<StackLayout Margin="20,0,20,0"
Spacing="0">
<Label Text="Title"/>
<Entry />
</StackLayout>
<StackLayout Margin="20,20,20,0"
Spacing="0">
<Label Text="Description"/>
<Entry />
</StackLayout>
<StackLayout Margin="20,20,20,0"
Spacing="0">
<Label Text="Type"/>
<Picker ItemsSource="{Binding ItemModel.Type}" />
</StackLayout>
<StackLayout Margin="20,20,20,0"
Spacing="0">
<Label Text="Price"/>
<Entry Keyboard="Numeric"/>
</StackLayout>
<Button Command="{Binding SaveCommand}"
Text="Save"
VerticalOptions="End">
</Button>
</StackLayout>
</ContentPage.Content>
And here is the NewGoalViewModel
class NewGoalViewModel : MainViewModel
{
public Item ItemModel { get; set; }
public Command SaveCommand { get; set; }
public NewGoalViewModel()
{
ItemModel = new Item();
ItemModel.Title = "Title";
ItemModel.Description = "Description";
ItemModel.Type = SavingsType.Other;
ItemModel.Price = 19.00f;
ItemModel.ImageSource = "cash.jpg";
SaveCommand = new Command(() => AddGoal());
}
private void AddGoal()
{
Items.Add(new Item { Title = "Rainy Day", Type = SavingsType.Other, Price = 100.00, ImageSource = "cash.jpg" });
}
}
The issue
So when I start the app and I click the first button, it takes me to the next page.
when I land on that page, it should show me an Image at the top. It's bound to a property which I've assigned in the constructor ItemModel.ImageSource = "cash.jpg";
The issue is however is that it doesnt actually bind, resulting in the image not showing until I save the NewGoal.xaml page and it reloads. Once it's done reloading it shows the image.
Try modifying
private Item itemModel { get; set; }
public Item ItemModel
{
get { return itemModel ; }
set
{
itemModel = value;
OnPropertyChanged();
}
}
and inherit your ViewModel to : INotifyPropertyChanged
and add this to implement
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
var changed = PropertyChanged;
if (changed == null)
return;
changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

Binding IsVisible to property toggled by Switch

I have a Switch bound to a property of an element in a List. I want to bind IsVisible of a button to the same property, but the button's visibility is not changed when the property is changed by the Switch. What am I missing?
XAML:
<StackLayout>
<ListView HorizontalOptions="FillAndExpand" ItemsSource="{Binding EquipmentList}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Horizontal">
<Label Text="{Binding Name}" />
<Switch IsToggled="{Binding State}" />
<Button
Command="{Binding BindingContext.DoCommand, Source={x:Reference TestPage}}"
CommandParameter="{Binding .}"
IsVisible="{Binding State}"
Text="Click" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
ViewModel:
private Command<Equipment> _doCommand;
public Command<Equipment> DoCommand => _doCommand ??
(_doCommand = new Command<Equipment>((Equipment obj) => HandleEquipment(obj)));
// Outputs correct Name and State of the list item
private void HandleEquipment(Equipment obj)
{
System.Diagnostics.Debug.WriteLine(obj.Name + ", " + obj.State);
}
Model:
class Equipment
{
public int Id { get; set; }
public string Name { get; set; }
public bool State { get; set; }
public Equipment(int Id, string Name, bool State)
{
this.Id = Id;
this.Name = Name;
this.State = State;
}
}
As Gerald wrote in his first comment: You have to implement the INotifyPropertyChanged interface on your Equipment model (and not just in the ViewModel).
Without this implementation, the elements in the view have no chance to know, that the state changed (in your case the button).
Implementation:
public class Equipment: INotifyPropertyChanged
{
public bool State
{
get => _state;
set =>
{
_state = value;
OnPropertyChanged();
}
}
private bool _state;
// OTHER PROPERTIES
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
The call of the method OnPropertyChanged() is important. The IsVisible property of the button recognizes the change and updates his value.
Instead of binding two things to a property, why not have the single item bound (i.e. the switch) and use XAML to show or hide the button:
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibility" />
</Window.Resources>
<StackLayout>
<ListView HorizontalOptions="FillAndExpand" ItemsSource="{Binding EquipmentList}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Horizontal">
<Label Text="{Binding Name}" />
<Switch Name="toggleSwitch" IsToggled="{Binding State}" />
<Button
Command="{Binding BindingContext.DoCommand, Source={x:Reference TestPage}}"
CommandParameter="{Binding .}"
IsVisible="{Binding ElementName=toggleSwitch, Path=IsToggled, Converter={StaticResource BooleanToVisibilityConverter}"
Text="Click" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
It may not be a Window that your StackLayout is in, but if you place a BooleanToVisibilityConverter in your Resources section you'll then be able to use it in your XAML file.
This will mean that if the property name changes in the future you only have one place you need to update in the user interface and you're also using the power of the XAML language.
Also as correctly pointed out by everyone, you need to implement INotifyPropertyChanged in the model in order for the Switch to be updated too.

UWP ListView Item context menu

I'm searching internet for how to add context menu for ListView. So far I've found one that actually displays context
<ListView>
...
RightTapped="ContactsListView_RightTapped" >
...
<ListView.Resources>
<MenuFlyout x:Name="allContactsMenuFlyout">
<MenuFlyout.Items>
<MenuFlyoutItem x:Name="Edit" Text="Edit"/>
<MenuFlyoutItem x:Name="Remove" Text="Remove" Click="Remove_Click"/>
</MenuFlyout.Items>
</MenuFlyout>
</ListView.Resources>
...
</ListView>
private void ContactsListView_RightTapped(object sender, RightTappedRoutedEventArgs e) {
ListView listView = (ListView)sender;
allContactsMenuFlyout.ShowAt(listView, e.GetPosition(listView));
}
private void Remove_Click(object sender, RoutedEventArgs e) {
}
The problem is I'm not able to get item on which the context menu was displayed. Another issue is that the context menu is displayed also outside of list view item (e.g. on borders). And since the event that is triggered is RightTapped, I'm not sure if the context menu would be displayed on long click on mobile devices. I cannot test it because my emulators are not currently working. Since it should be universal windows app I was expecting some really easy and efficient way of creating context menus for ListView items.
The problem is I'm not able to get item on which the context menu was displayed.
For this problem, if you add data to the ListView like this:
<ListView RightTapped="ListView_RightTapped">
<x:String>First Item</x:String>
<x:String>Second Item</x:String>
<x:String>Third Item</x:String>
<x:String>Fourth Item</x:String>
<ListView.Resources>
<MenuFlyout x:Name="allContactsMenuFlyout">
<MenuFlyout.Items>
<MenuFlyoutItem x:Name="Edit" Text="Edit" />
<MenuFlyoutItem x:Name="Remove" Text="Remove" Click="Remove_Click" />
</MenuFlyout.Items>
</MenuFlyout>
</ListView.Resources>
</ListView>
You can get the item's context in the RightTapped event like this:
private void ListView_RightTapped(object sender, RightTappedRoutedEventArgs e)
{
ListView listView = (ListView)sender;
allContactsMenuFlyout.ShowAt(listView, e.GetPosition(listView));
var a = ((FrameworkElement)e.OriginalSource).DataContext;
}
In this scenario, "a" will directly get the string format content of clicked item.
If you add your data to ListView using DataTemplate like this:
<ListView RightTapped="ListView_RightTapped" ItemsSource="{x:Bind list}">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding text}" />
</DataTemplate>
</ListView.ItemTemplate>
<ListView.Resources>
<MenuFlyout x:Name="allContactsMenuFlyout">
<MenuFlyout.Items>
<MenuFlyoutItem x:Name="Edit" Text="Edit" />
<MenuFlyoutItem x:Name="Remove" Text="Remove" Click="Remove_Click" />
</MenuFlyout.Items>
</MenuFlyout>
</ListView.Resources>
</ListView>
and usually when using DataTemplate, we add data by ObservableCollection like this:
private ObservableCollection<List> list = new ObservableCollection<List>();
public MainPage()
{
this.InitializeComponent();
list.Clear();
list.Add(new List { text = "Item 1" });
list.Add(new List { text = "Item 2" });
list.Add(new List { text = "Item 3" });
list.Add(new List { text = "Item 4" });
list.Add(new List { text = "Item 5" });
}
"List" class is quite simple here for test:
public class List
{
public string text { get; set; }
}
Then also we can get the DataContext in the RightTapped event:
private void ListView_RightTapped(object sender, RightTappedRoutedEventArgs e)
{
ListView listView = (ListView)sender;
allContactsMenuFlyout.ShowAt(listView, e.GetPosition(listView));
var a = ((FrameworkElement)e.OriginalSource).DataContext;
}
But this time, "a" is actually the 'List' object (please refer to the "List" class) inside the item, because the content of the item is now a 'List' object, not a string any more. So we can get the text property of this object like this:
private void ListView_RightTapped(object sender, RightTappedRoutedEventArgs e)
{
ListView listView = (ListView)sender;
allContactsMenuFlyout.ShowAt(listView, e.GetPosition(listView));
var a = ((FrameworkElement)e.OriginalSource).DataContext as List;
var content = a.text;
}
I think eventually you want to edit the content in the Button click event of the Flyout, you can do it for example like this:
private string content;
private void ListView_RightTapped(object sender, RightTappedRoutedEventArgs e)
{
ListView listView = (ListView)sender;
allContactsMenuFlyout.ShowAt(listView, e.GetPosition(listView));
var a = ((FrameworkElement)e.OriginalSource).DataContext as List;
content = a.text;
}
private void Remove_Click(object sender, RoutedEventArgs e)
{
foreach (var item in list.ToList())
{
if (item.text == content)
{
list.Remove(item);
}
}
content = "";
}
Another issue is that the context menu is displayed also outside of list view item (e.g. on borders).
Can you explain this? I can't quite understand it. You mean displaying the content for example in the Flyout? If so, I think the method above can solve this problem. If not, you can leave a comment, and I will see if this problem can be resolved.
And since the event that is triggered is RightTapped, I'm not sure if the context menu would be displayed on long click on mobile devices.
I think that "long click" event here indicates the Holding event like this?
private void ListView_Holding(object sender, HoldingRoutedEventArgs e)
{
ListView listView = (ListView)sender;
allContactsMenuFlyout.ShowAt(listView, e.GetPosition(listView));
var a = ((FrameworkElement)e.OriginalSource).DataContext as List;
content = a.text;
}
I just test it on the Mobile Emulator, it works fine. Although I wrote a quite long answer here, but the key point is quite simple, you can just use ((FrameworkElement)e.OriginalSource).DataContext to get the Context of the item.
Use Command instead of Click event. You can pass the clicked item in CommandParameter
<MenuFlyout x:Name="allContactsMenuFlyout">
<MenuFlyout.Items>
<MenuFlyoutItem x:Name="Edit" Text="Edit"/>
<MenuFlyoutItem x:Name="Remove" Text="Remove" Command="{Binding Path=DeleteItemTappedCommand}" CommandParameter="{Binding ElementName=ArchivedMessages_ListView, Path=SelectedItem}"/>
</MenuFlyout.Items>
</MenuFlyout>
Inside your ViewModel
public DelegateCommand<object> DeleteItemTappedCommand { get; set; }
public YourViewModel()
{
DeleteItemTappedCommand = new DelegateCommand<object>(DeleteItemClicked);
}
private void DeleteItemClicked(object obj)
{
// adjust object type to your templated source type
}
or for the CommunityToolkit.MVVM users:
[ICommand]
private void DeleteItemClicked(object obj)
{
// adjust object type to your templated source type
}
Add flyout in the datatemplate. Use command to deal with the events.
See sample code here:
<DataTemplate x:Name="ListItemTemplate" >
<Grid x:Name="gridItem" RightTapped="gridItem_RightTapped">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Image Name="imgProduct" Width="50" Height="50" Grid.Column="0" Source="{Binding ProductUrl}" Margin="0,5,10,5" VerticalAlignment="Center" ></Image>
<TextBlock Name="tbName" Text="{Binding Name}" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Stretch" ></TextBlock>
<FlyoutBase.AttachedFlyout>
<MenuFlyout>
<MenuFlyoutItem Text="Delete" Command="{Binding DataContext.DeleteCommand, ElementName=contentGrid}" CommandParameter="{Binding}" />
</MenuFlyout>
</FlyoutBase.AttachedFlyout>
</Grid>
</DataTemplate>
Code behind:
private void gridItem_RightTapped(object sender, RightTappedRoutedEventArgs e)
{
FlyoutBase.ShowAttachedFlyout(sender as FrameworkElement);
}
You can get the full solution here: https://code.msdn.microsoft.com/How-to-implement-flyout-ef52517f

XAML ListBox displays class name instead of class property

Here I have a Listbox configured where the TextBlox in the DataTemplate is set to bind the "Name" Property. But instead it shows the full class name "DomainClasses.Entities.Program". Why?
<Grid DataContext="{Binding _CurrentProgram }">
.....
.....
<ListBox x:Name="ProgramsListBox" Width="600" Height="400" Margin="50,0,50,0" ItemsSource="{Binding _Programs}" VerticalAlignment="Top">
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}"></TextBlock>
</StackPanel>
<DataTemplate>
</ListBox>
----
----
</Grid>
This is the ViewModel class
public class MainPageViewModel : INotifyPropertyChanged
{
public MainPageViewModel()
{
_currentProgram = new Program();
_Programs = new ObservableCollection<Program>();
}
public async void SaveProgram(bool isEditing)
{
_Programs.Add(_currentProgram);
OnPropertyChanged();
}
private Program _currentProgram;
public Program _CurrentProgram
{
get { return _currentProgram; }
set
{
_currentProgram = value;
OnPropertyChanged();
}
}
private ObservableCollection<Program> _programs;
public ObservableCollection<Program> _Programs
{
get
{
return _programs;
}
set
{
this._programs = value;
OnPropertyChanged();
}
}
// Implement INotifyPropertyChanged Interface
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string caller = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(caller));
}
}
}
This is what you need:
<ListBox>
<ListBox.ItemTemplate>
<DataTemplate>
<Button/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Noticed the ListBox.ItemTemplate around the DataTemplate.
What you have:
<ListBox x:Name="ProgramsListBox" Width="600" Height="400" Margin="50,0,50,0" ItemsSource="{Binding _Programs}" VerticalAlignment="Top">
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}"></TextBlock>
</StackPanel>
<DataTemplate>
</ListBox>
Creates a ListBox with a DataTemplate as a child (in the same sense that the items in the ItemsSource are children of the ListBox). If I remember correctly, when you set the ItemsSource of a ListBox, all items set in the other fashion are removed. So what you're ending up with is a ListBox with a bunch of Programs in it, which no ItemsTemplate set, so it simply shows the name of the bound class.
You need to add the data template inside listview.itemtemplate and then do the binding. Right now you are adding the data template as a child of the listview.

Get SelectedItem from ListBox

I want to get the SelectedItem from a ListBox which looks like this inside my Windows 8 Store App:
<ListBox x:Name="listBox" SelectedItem="{Binding SelectedItem, Mode=TwoWay}" Foreground="Black" BorderThickness="0" Background="#FFD8D8D8" />
The problem is, that the ListBox don't fire the SelectedItem propertie. I have to use IsSynchronizedWithCurrentItem="True" but then an error appears which says the true isn't supportet for this property. What do I have to do or are there any other ways to get the SelectedItem propertie?
I have this Code behind:
namespace ExampleApp
{
public sealed partial class MainPage : Page, INotifyPropertyChanged
{
private object currentItem;
//Constructor and so on
public object SelectedItem
{
get { Debug.WriteLine("get"); return currentItem; }
set { Debug.WriteLine("set"); currentItem = value; NotifyPropertyChanged(); }
}
}
}
you should try this
<ListBox x:Name="listBox" SelectedItem="{Binding ElementName=YourPageName,path=DataContext.SelectedItem, Mode=TwoWay}" Foreground="Black" BorderThickness="0" Background="#FFD8D8D8" />