I am trying to create a custom view that will be used as a header in some of the pages in the application. A custom view has a button to save info, and an image to show if the info was saved, but I can also receive info from the API if the info was saved. (this is a simplified version of the scenario)
So, I have MainPage.xaml (any page that will use the custom view)
ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Messages"
xmlns:controls="clr-namespace:Messages.Controls"
x:Class="Messages.MainPage">
<StackLayout Spacing="5">
<controls:HeaderMenu x:Name="menu" HorizontalOptions="FillAndExpand" VerticalOptions="Start" SaveCommand="{Binding MyCommand}" IsControlClosed="{Binding ControlClosedValue, Mode=TwoWay}" />
.....
</StackLayout>
MainPageViewModel.cs
public class MainPageViewModel : INotifyPropertyChanged
{
public ICommand MyCommand { get; set; }
private bool _controlClosedvalue;
public bool ControlClosedValue
{
get => _controlClosedvalue;
set
{
_controlClosedvalue = value;
OnPropertyChanged(nameof(ControlClosedValue));
}
}
public MainPageViewModel()
{
MyCommand = new Command(MyCommandExecute);
_controlClosedvalue = false;
}
private void MyCommandExecute()
{
// do stuff
_controlClosedvalue = true; //change value to change the value of control
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
HeaderMenu.xaml
<Grid>
<Image Source="save.png" HeightRequest="25" WidthRequest="25">
<Image.GestureRecognizers>
<TapGestureRecognizer NumberOfTapsRequired="1" Tapped="SaveImage_OnTapped" />
</Image.GestureRecognizers>
</Image>
<Image IsVisible="{Binding IsControlClosed}" Source="check.png" HeightRequest="30" WidthRequest="30" />
HeaderMenu.xaml.cs
public partial class HeaderMenu : ContentView
{
public HeaderMenu ()
{
InitializeComponent();
imgControlClosed.BindingContext = this;
}
public static readonly BindableProperty SaveCommandProperty =
BindableProperty.Create(nameof(SaveCommand), typeof(ICommand), typeof(HeaderMenu));
public static readonly BindableProperty IsControlClosedProperty =
BindableProperty.Create(nameof(IsControlClosed), typeof(bool), typeof(HeaderMenu), false, BindingMode.TwoWay, null, ControlClosed_OnPropertyChanged);
public ICommand SaveCommand
{
get => (ICommand) GetValue(SaveCommandProperty);
set => SetValue(SaveCommandProperty, value);
}
public bool IsControlClosed
{
get => (bool) GetValue(IsControlClosedProperty);
set => SetValue(IsControlClosedProperty, value);
}
private static void ControlClosed_OnPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is HeaderMenu control)
{
control.imgControlClosed.IsVisible = (bool)newValue;
}
}
private void SaveImage_OnTapped(object sender, EventArgs e)
{
if (SaveCommand != null && SaveCommand.CanExecute(null))
{
SaveCommand.Execute(null);
}
}
}
So, what I need is that when the save command is tapped to execute some code in the page that is using control, and binding of SaveCommand works as expected. But after the code is executed, or in some different cases, I wish to change the property in the page model and this should change the property on the custom view, but this does not work.
Does anyone know what is wrong with this code?
If I just put True or False when consuming control it works.
<controls:HeaderMenu x:Name="menu" HorizontalOptions="FillAndExpand" VerticalOptions="Start" SaveCommand="{Binding MyCommand}" IsControlClosed="True" />
But it does not work when binding it to the property.
I have found out what an issue was. A stupid mistake, I was setting the value of the variable instead of property.
In the main page view model, instead of
_controlClosedvalue = false; // or true
it should be
ControlClosedValue = false; // or true
Related
I'm struggeling a bit with a bindable property and the propertyChanged event not firing when new text is entered.
I've made a minimal codesample:
Xaml custom control:
<Grid xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="BindingPropertyProject.CustomFlyout">
<Entry x:Name="MyEntry"/>
Codebehind:
public partial class CustomFlyout : Grid
{
public CustomFlyout()
{
InitializeComponent();
}
public string MyEntryText
{
get { return (string)GetValue(MyEntryTextProperty); }
set
{
SetValue(MyEntryTextProperty, value);
}
}
public static readonly BindableProperty MyEntryTextProperty =
BindableProperty.Create(nameof(MyEntryText), typeof(string),
typeof(CustomFlyout),
defaultValue: string.Empty,
defaultBindingMode: BindingMode.TwoWay
, propertyChanging: TextChanged);
private static void TextChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is CustomFlyout control)
{
control.MyEntry.Text = newValue?.ToString();
}
}
}
}
Consuming class xaml:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:BindingPropertyProject"
x:Class="BindingPropertyProject.MainPage">
<Grid>
<local:CustomFlyout MyEntryText="{Binding TextPropertyFromBindingContext, Mode=TwoWay}" HorizontalOptions="FillAndExpand" VerticalOptions="Start"/>
</Grid>
Consuming class codebehind:
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
BindingContext = this;
}
private string _textPropertyFromBindingContext = "bound";
public string TextPropertyFromBindingContext
{
get
{
return _textPropertyFromBindingContext;
}
set
{
if (_textPropertyFromBindingContext != value)
{
_textPropertyFromBindingContext = value;
OnPropertyChanged();
}
}
}
}
It binds the "bound" value just fine, but subsequent changes entered in the entry does not raise property changed.
I've tried a number of suggestions i found from googeling, but this should be fine right?
UPDATE:
Ok - so i actually got i to work by adding binding in the custom view:
<Grid xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="BindingPropertyProject.CustomFlyout">
<Entry x:Name="MyEntry" Text="{Binding TextPropertyFromBindingContext }"/>
Is this really the way to do it? I mean - i could only make it work, if bindings was named EXACTLY the same in custom view, and consuming part..
i could only make it work, if bindings was named EXACTLY the same in
custom view, and consuming part..
It's not necessary to have same binding name. Please refer following code.
Custom Control
<ContentView xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="StackQA2XF.CustomControl.MyCustomControl">
<ContentView.Content>
<Entry x:Name="CustomEntry"/>
</ContentView.Content>
</ContentView>
public partial class MyCustomControl : ContentView
{
public static readonly BindableProperty EntryTextProperty =
BindableProperty.Create(nameof(EntryText), typeof(string), typeof(MyCustomControl), default(string), BindingMode.TwoWay);
public string EntryText
{
get { return (string)GetValue(EntryTextProperty); }
set { SetValue(EntryTextProperty, value); }
}
public MyCustomControl()
{
InitializeComponent();
CustomEntry.SetBinding(Entry.TextProperty, new Binding(nameof(EntryText), source: this));
}
}
Consuming Class
<customcontrols:MyCustomControl EntryText="{Binding TitleText}"/>
public class MainViewModel : INotifyPropertyChanged
{
private string _titleText = "Good morning";
public string TitleText
{
get
{
return _titleText;
}
set
{
_titleText = value;
OnPropertyChange(nameof(TitleText));
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChange(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
}
Please do binding in the code for custom control and raise property change for the binding property in viewmodel.
CustomEntry.SetBinding(Entry.TextProperty, new Binding(nameof(EntryText), source: this));
OnPropertyChange(nameof(TitleText));
Please refer https://www.youtube.com/watch?v=ZViJyL9Ptqg.
I have tested this code able to get fired propertyChanged event when Entry text is changed from custom view.
It binds the "bound" value just fine, but subsequent changes entered in the entry does not raise property changed.
From Bindable Properties property changes, BindableProperty MyEntryTextProperty binding TextPropertyFromBindingContext, so the propertyChanged event will be fired when you change TextPropertyFromBindingContext, Instead of changing the value of MyEntry.
You can change TextPropertyFromBindingContext bu Button.click, then you will see the propertyChanged event will be fired.
public partial class Page3 : ContentPage, INotifyPropertyChanged
{
private string _textPropertyFromBindingContext = "bound";
public string TextPropertyFromBindingContext
{
get
{
return _textPropertyFromBindingContext;
}
set
{
if (_textPropertyFromBindingContext != value)
{
_textPropertyFromBindingContext = value;
RaisePropertyChanged("TextPropertyFromBindingContext");
}
}
}
public Page3()
{
InitializeComponent();
this.BindingContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
private void btn1_Clicked(object sender, EventArgs e)
{
TextPropertyFromBindingContext = "test";
}
}
In my xaml file i have
<Button
Command="{Binding LoginCommand}"
</Button>
<Frame
Grid.Column="1"
NavigationUIVisibility="Hidden"
Content="{Binding CurrentPage, UpdateSourceTrigger=PropertyChanged}">
</Frame>
My viewmodel class:
private readonly LoginPageView _loginPage; // Page i want to use
private readonly StartPageView _startPage;
private Page _currentPage;
public StartWindowViewModel(LoginPageView loginPage, StartPageView startPage)
{
_loginPage = loginPage;
_startPage = startPage;
CurrentPage = _startPage;
LoginCommand = new RelayCommand(OpenLoginPage, _ => true);
}
public event PropertyChangedEventHandler PropertyChanged;
public Page CurrentPage // I'm binding my frame to this property
{
get => _currentPage;
set
{
_currentPage = value;
OnPropertyChanged();
}
}
public RelayCommand LoginCommand { get; set; }
private void OpenLoginPage(object parameter)
{
CurrentPage = _loginPage;
}
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
I want to change page displayed in frame by clicking a button. I'm not sure if i should user Frame for this, but it seems okay. The problem that i'm not updating page,
My solution not seems to be best possible:
<Frame
Grid.Column="1"
NavigationUIVisibility="Hidden"
Source="{Binding PageName}">
</Frame>
public string PageName
{
get => _pageName + ".xaml";
set
{
_pageName = value;
OnPropertyChanged();
}
}
I don't actualy like this way of solving, so i would be happy if somebody would give a better approach.
I have a shared view for Add and Detail page. For some reason in the detail page, the view model won't binding to this child view (page come up blank as in NO populated value from the api service). Any ideas?
Debug this and there was a data coming from web api for both CategoryList as well as _activity.
How to debug this binding process?
ActivityView.xaml
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="AthlosifyMobileApp.Views.ActivityView">
<StackLayout Spacing="12">
<Entry x:Name="txtName" Text="{Binding Name}" HeightRequest="40" BackgroundColor="White" Placeholder="Name" HorizontalOptions="FillAndExpand"/>
<Entry x:Name="txtNoOfMinutes" Keyboard="Numeric" Text="{Binding NoOfMinutes}" BackgroundColor="White" Placeholder="NoOfMinutes" HorizontalOptions="FillAndExpand"/>
<Entry x:Name="txtDescription" Text="{Binding Description}" HeightRequest="40" BackgroundColor="White" Placeholder="Description" HorizontalOptions="FillAndExpand"/>
<Picker ItemsSource="{Binding CategoryList}" ItemDisplayBinding="{Binding Name}" SelectedItem="{Binding SelectedCategory}"></Picker>
</StackLayout>
</ContentView>
ActivityView.xaml.cs
namespace AthlosifyMobileApp.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class ActivityView : ContentView
{
public ActivityView ()
{
InitializeComponent ();
}
}
}
ActivityDetailViewModel.cs
namespace AthlosifyMobileApp.ViewModels
{
public class ActivityDetailViewModel : ActivityBaseViewModel
{
public ICommand DeleteCommand { get; private set; }
public ICommand UpdateCommand { get; private set; }
public ActivityDetailViewModel(INavigation navigation, int selectedActivityId)
{
_navigation = navigation;
_activityValidator = new ActivityValidator();
_activity = new Activity();
_activity.Id = selectedActivityId;
_apiService = new ApiService();
//DeleteCommand = new Command(async () => await HandleDeleteActivity());
UpdateCommand = new Command(async () => await UpdateActivity());
FetchActivityDetail();
FetchCategories();
}
async void FetchActivityDetail()
{
_activity = await _apiService.GetActivity(_activity.Id);
}
async void FetchCategories()
{
CategoryResult categoryResult = await _apiService.GetCategories();
CategoryList = categoryResult.Results;
}
async Task UpdateActivity()
{
_activity.OwnerId = Preferences.Get(Constant.Setting_UserId, "");
_activity.CategoryId = SelectedCategory.Id;
_activity.CategoryName = SelectedCategory.Name;
var validationResults = _activityValidator.Validate(_activity);
if (validationResults.IsValid)
{
bool isUserAccept = await Application.Current.MainPage.DisplayAlert("Contact Details", "Update Contact Details", "OK", "Cancel");
if (isUserAccept)
{
var response = await _apiService.UpdateActivity(_activity.Id,_activity);
if (!response)
{
await Application.Current.MainPage.DisplayAlert("Add Activity", "Error", "Alright");
}
else
{
await _navigation.PushAsync(new ActivityListPage());
}
await _navigation.PopAsync();
}
}
else
{
await Application.Current.MainPage.DisplayAlert("Add Contact", validationResults.Errors[0].ErrorMessage, "Ok");
}
}
public async Task HandleDeleteActivity(int id)
{
var alert = await Application.Current.MainPage.DisplayAlert("Warning", "Do you want to delete this item?", "Yes", "Cancel");
if (alert)
{
var response = await _apiService.DeleteActivity(id);
if (!response)
{
await Application.Current.MainPage.DisplayAlert("Error", "Something wrong", "Alright");
}
else
{
await _navigation.PushAsync(new ActivityListPage());
}
}
}
}
}
ActivityBaseViewModel.cs
namespace AthlosifyMobileApp.ViewModels
{
public class ActivityBaseViewModel : INotifyPropertyChanged
{
public Activity _activity;
public INavigation _navigation;
public IValidator _activityValidator;
public ApiService _apiService;
public string Name
{
get
{
return _activity.Name;
}
set
{
_activity.Name = value;
NotifyPropertyChanged("Name");
}
}
public string Description
{
get { return _activity.Description; }
set
{
_activity.Description = value;
NotifyPropertyChanged("Description");
}
}
public int NoOfMinutes
{
get { return _activity.NoOfMinutes; }
set
{
_activity.NoOfMinutes = value;
NotifyPropertyChanged("NoOfMinutes");
}
}
public int CategoryId
{
get { return _activity.CategoryId; }
set
{
_activity.CategoryId = value;
NotifyPropertyChanged("CategoryId");
}
}
public string CategoryName
{
get { return _activity.CategoryName; }
set
{
_activity.CategoryName = value;
NotifyPropertyChanged("CategoryName");
}
}
//List<Activity> _activityList;
InfiniteScrollCollection<Activity> _activityList;
//public List<Activity> ActivityList
public InfiniteScrollCollection<Activity> ActivityList
{
get => _activityList;
set
{
_activityList = value;
NotifyPropertyChanged("ActivityList");
}
}
List<Category> _categoryList;
public List<Category> CategoryList
{
get { return _categoryList; }
set
{
_categoryList = value;
NotifyPropertyChanged("CategoryList");
}
}
public Category SelectedCategory
{
get
{
return _activity.SelectedCategory;
}
set
{
_activity.SelectedCategory = value;
NotifyPropertyChanged("SelectedCategory");
}
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}
ActivityDetailPage.xaml
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:AthlosifyMobileApp.Views"
x:Class="AthlosifyMobileApp.Views.ActivityDetailPage"
Title="Detail Activity">
<ContentPage.ToolbarItems>
<ToolbarItem Command="">
<ToolbarItem.IconImageSource>
<FontImageSource Glyph="" FontFamily="{StaticResource MaterialFontFamily}"/>
</ToolbarItem.IconImageSource>
</ToolbarItem>
<ToolbarItem Command="{Binding UpdateCommand}">
<ToolbarItem.IconImageSource>
<FontImageSource Size="30" Glyph="" FontFamily="{StaticResource MaterialFontFamily}"/>
</ToolbarItem.IconImageSource>
</ToolbarItem>
</ContentPage.ToolbarItems>
<ContentPage.Content>
<StackLayout Padding="20" Spacing="12">
<local:ActivityView />
</StackLayout>
</ContentPage.Content>
</ContentPage>
ActivityDetailPage.xaml.cs
namespace AthlosifyMobileApp.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class ActivityDetailPage : ContentPage
{
public ActivityDetailPage(int activityId)
{
InitializeComponent ();
BindingContext = new ActivityDetailViewModel(Navigation, activityId);
}
}
}
Based on the code you shared, I think it's likely you're not seeing any data on the activity detail page because you are fetching the data via an async method that is not awaited (FetchActivityData). Btw, async void method should be avoided if possible. There is no way to catch/handle exceptions thrown from them.
It looks like you are not awaiting because you are calling from the constructor of your viewmodel. What actually happens here is that the constructor returns immediately, while FetchActivityDetail() and FetchCategories() continue to run in the background. The page is displayed, but there is no data yet, so you don't see anything displayed. Then, when FetchActivityDetail completes, it sets _activity, but that's a field, so no PropertyChanged events are fired, so the page doesn't know it needs to update.
Here are a few suggestions:
Do not perform long-running processes (like fetching data) in constructors. Passing in the existing data (like your activityid), is generally ok, although it can make using dependency injection a bit harder, if you eventually want to do that.
When navigating to a viewmodel that requires fetching data, I generally recommend waiting until the view/vm are displayed before making the api call. To do this, I have all my views call an OnAppearing method in my viewmodels. This is easily moved into a BasePage and BaseViewModel that everything inherits from. Then, you can do things like setting an IsBusy status (to trigger some UI like a spinner), and populate your data. It could look something like this:
public override async Task OnAppearing()
{
await base.OnAppearing();
try
{
IsBusy = true;
await FetchActivityDetail();
await FetchCategories();
}
catch (Exception ex)
{
//handle/display error
}
finally
{
IsBusy = false;
}
}
Another option would be to make this a method that's called prior to navigation, but that would require creating the viewmodel first, which is a different navigation pattern than you're using here. There are some good examples out there of viewmodel-first navigation, but I won't go into that here.
Ensure that when data is fetched, it sets properties that cause PropertyChanged events to fire, so the view bindings update. You can't just set a backing field.
According to your description, you want to bind custom view in Xamarin.Forms, I suggest you don't assign binding internally inside custom controls, use this:
<ContentView
x:Class="demo2.simplecontrol.View1"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<ContentView.Content>
<StackLayout>
<Entry x:Name="label1" />
<Entry x:Name="label2" />
</StackLayout>
</ContentView.Content>
public partial class View1 : ContentView
{
public View1 ()
{
InitializeComponent ();
}
public static readonly BindableProperty Label1Property= BindableProperty.Create(
nameof(Label1),
typeof(string),
typeof(View1),
"",
BindingMode.TwoWay,
propertyChanged: (bindable, oldValue, newValue) =>
{
if (newValue != null && bindable is View1 control)
{
var actualNewValue = (string)newValue;
control.label1.Text = actualNewValue;
}
});
public string Label1 { get; set; }
public static readonly BindableProperty Label2Property = BindableProperty.Create(
nameof(Label2),
typeof(string),
typeof(View1),
"",
BindingMode.TwoWay,
propertyChanged: (bindable, oldValue, newValue) =>
{
if (newValue != null && bindable is View1 control)
{
var actualNewValue = (string)newValue;
control.label2.Text = actualNewValue;
}
});
public string Label2 { get; set; }
}
Then you can use this custom view in ContentPage.
<ContentPage
x:Class="demo2.simplecontrol.Page10"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:demo2.simplecontrol">
<ContentPage.Content>
<StackLayout>
<Label
HorizontalOptions="CenterAndExpand"
Text="Welcome to Xamarin.Forms!"
VerticalOptions="CenterAndExpand" />
<local:View1 Label1="{Binding text1}" Label2="{Binding text2}" />
</StackLayout>
</ContentPage.Content>
public partial class Page10 : ContentPage, INotifyPropertyChanged
{
private string _text1;
public string text1
{
get { return _text1; }
set
{
_text1 = value;
RaisePropertyChanged("text1");
}
}
private string _text2;
public string text2
{
get { return _text2; }
set
{
_text2 = value;
RaisePropertyChanged("text2");
}
}
public Page10 ()
{
InitializeComponent ();
text1 = "test1";
text2 = "test2";
this.BindingContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Finally, you get data coming from web api for CategoryList, so you can add break point to check if having data.
'I'm not sure, but apparently the page is changing the binding context of your view.
Set a breakpoint inside your OnBindingContextChanged overridden method of your view and debug it. If it is confirmed, instantiate your view model from your page instead.
I'm developing an app with xamarin forms and the MVVM pattern. I have a page with a listview that has three buttons but all the time with only 2 visibles and change the visibility of two of them when I press a button. The problem is that for the first ten items it works like supposed to be, press the button and dissapear and appear the other, but after the 10th item when I press the button it dissapear but the other doesn't appear until I scrool the list view to a position where the item is out of the screen. When the item is out of the screen and come back to be on the screen, the button appear. The visibility of the buttons is controlled changing a boolean property that is binded to the IsVisible property of the button and one of them with a converter to negate the value of the property. This is a repository that you can clone and see the code and test, maybe is something with my Visual Studio.
Initially, I thought it could be for a race condition and made the method that change the variable synchronous but it doesn't work.
This is my list view
<ListView ItemsSource="{Binding Items}"
HasUnevenRows="True"
SeparatorVisibility="None"
IsRefreshing="False">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout>
<Label Text="{Binding Name}"/>
<StackLayout Orientation="Horizontal">
<Button Text="One"
HorizontalOptions="CenterAndExpand"
TextColor="Green"
BackgroundColor="White"
BorderColor="Green"
BorderWidth="1"
WidthRequest="150" />
<Button Text="Two"
HorizontalOptions="CenterAndExpand"
BackgroundColor="Green"
TextColor="White"
Command="{Binding TestCommand}"
WidthRequest="150"
IsVisible="{Binding TestVariable, Converter={StaticResource negate}}" />
<Button Text="Three"
HorizontalOptions="CenterAndExpand"
BackgroundColor="Red"
Command="{Binding TestCommand}"
TextColor="White"
WidthRequest="150"
IsVisible="{Binding TestVariable}" />
</StackLayout>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
The viewmodel
public class ListViewTestModel : BaseViewModel
{
private List<ListItemTestModel> items;
public List<ListItemTestModel> Items
{
get => items;
set
{
SetValue(ref items, value);
}
}
public ListViewTestModel()
{
List<ListItemTestModel> itemList = new List<ListItemTestModel>();
for (int i = 0; i < 40; i++)
{
itemList.Add(new ListItemTestModel { Name = "Test" });
}
Items = itemList;
}
}
And another view model that is binded to each item in the listView
public class ListItemTestModel : BaseViewModel
{
private bool testVariable;
public string Name { get; set; }
public bool TestVariable
{
get
{
return testVariable;
}
set
{
SetValue(ref testVariable, value);
}
}
public Command TestCommand { get; set; }
public ListItemTestModel()
{
TestCommand = new Command(() =>
{
TestMethod();
});
}
public void TestMethod()
{
TestVariable = !TestVariable;
}
}
the BaseViewModel
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected void SetValue<T>(ref T backingField, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(backingField, value))
{
return;
}
backingField = value;
OnPropertyChanged(propertyName);
}
}
And the codebehind of the page
public partial class MainPage : ContentPage
{
public ListViewTestModel ViewModel { get; }
public MainPage()
{
ViewModel = new ListViewTestModel();
BindingContext = ViewModel;
InitializeComponent();
}
}
I suggest listview Caching Strategy may case this issue, the default value is RetainElement for ListView, so using CachingStrategy="RecycleElement" in ListView.
About listview Caching Strategy, you can take a look:
https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/listview/performance#caching-strategy
You should definitely go to ObservableCollection type for your items thus you'll be able to observe and display any changes
private ObservableCollection<ListItemTestModel> items;
public ObservableCollection<ListItemTestModel> Items
{
get => items;
set => SetValue(ref items, value);
}
And you should set your BindingContext AFTER the InitializeComponent() method or property changed will be propagate before your view is initialized.
public MainPage()
{
InitializeComponent();
BindingContext = new ListViewTestModel();;
}
public ListViewTestModel()
{
List<ListItemTestModel> itemList = new List<ListItemTestModel>();
for (int i = 0; i < 40; i++)
{
itemList.Add(new ListItemTestModel { Name = "Test" });
}
Items = new ObservableCollection<ListItemTestModel>(itemList);
}
I'm beginner in WPF and MVVM, but want to learn it by building some small project.
I've got a WPF app using the Model-View-ViewModel pattern, based on Rachel Lim example. In my app I have 2 views - EmployeesList and EmployeeDetails.
List of employees is storage in GidView.
The main problem I have is
How to change view when I double-click on a row,
How to get the value from the first column (employee_id) and pass it into EmployeeDetails view.
Base navigation is in xaml with DataTmplate and ItmCntrol:
<Window.Resources>
<DataTemplate DataType="{x:Type local:HomeViewModel}">
<local:HomeView />
</DataTemplate>
<DataTemplate DataType="{x:Type local:EmployeesListViewModel}">
<local:EmployeesListView />
</DataTemplate>
</Window.Resources>
<ItemsControl ItemsSource="{Binding PageViewModels}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Name}"
Command="{Binding DataContext.ChangePageCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
CommandParameter="{Binding }"
Margin="2,5"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
also I've got ApplicationViewModel where is list of views
public class ApplicationViewModel : ObservableObject
{
#region Fields
private ICommand _changePageCommand;
private IPageViewModel _currentPageViewModel;
private List<IPageViewModel> _pageViewModels;
#endregion
public ApplicationViewModel()
{
// Add available pages
PageViewModels.Add(new HomeViewModel());
PageViewModels.Add(new EmployeesListViewModel());
PageViewModels.Add(new EmployeeDetailsViewModel());
// Set starting page
CurrentPageViewModel = PageViewModels[0];
}
#region Properties / Commands
public ICommand ChangePageCommand
{
get
{
if (_changePageCommand == null)
{
_changePageCommand = new RelayCommand(
p => ChangeViewModel((IPageViewModel)p),
p => p is IPageViewModel);
}
return _changePageCommand;
}
}
public List<IPageViewModel> PageViewModels
{
get
{
if (_pageViewModels == null)
_pageViewModels = new List<IPageViewModel>();
return _pageViewModels;
}
}
public IPageViewModel CurrentPageViewModel
{
get
{
return _currentPageViewModel;
}
set
{
if (_currentPageViewModel != value)
{
_currentPageViewModel = value;
OnPropertyChanged("CurrentPageViewModel");
}
}
}
#endregion
#region Methods
private void ChangeViewModel(IPageViewModel viewModel)
{
if (!PageViewModels.Contains(viewModel))
PageViewModels.Add(viewModel);
CurrentPageViewModel = PageViewModels
.FirstOrDefault(vm => vm == viewModel);
}
#endregion
}
How to change view when I double-click on a row
First, you need to add EventTrigger for MouseDoubleClick event:
<DataGrid Name="gridEmployees" ItemsSource="{Binding Employees}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDoubleClick">
<local:CustomCommandAction Command="{Binding DoubleClickCommand}" CommandParameter="{Binding ElementName=gridEmployees, Path=SelectedItems[0]}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</DataGrid>
CustomCommandAction is a class, that inherits from TriggerAction and is used as a link between event and command in your View Model. Here is the code:
public sealed class CustomCommandAction : TriggerAction<DependencyObject>
{
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.Register("CommandParameter", typeof(object), typeof(CustomCommandAction), null);
public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(
"Command", typeof(ICommand), typeof(CustomCommandAction), null);
public ICommand Command
{
get
{
return (ICommand)this.GetValue(CommandProperty);
}
set
{
this.SetValue(CommandProperty, value);
}
}
public object CommandParameter
{
get
{
return this.GetValue(CommandParameterProperty);
}
set
{
this.SetValue(CommandParameterProperty, value);
}
}
protected override void Invoke(object parameter)
{
if (this.AssociatedObject != null)
{
ICommand command = this.Command;
if (command != null)
{
if (this.CommandParameter != null)
{
if (command.CanExecute(this.CommandParameter))
{
command.Execute(this.CommandParameter);
}
}
else
{
if (command.CanExecute(parameter))
{
command.Execute(parameter);
}
}
}
}
}
}
After that the easiest solution is to use ChangeViewModel method in yours command Execute method, e.g.:
...
_doubleClickCommand = new RelayCommand(OnDoubleClick);
...
private RelayCommand _doubleClickCommand = null;
private ApplicationViewModel _applicationViewModel;
private void OnDoubleClick(object obj)
{
EmployeeDetailsViewModel selectedModel = obj as EmployeeDetailsViewModel;
_applicationViewModel.ChangeViewModel(selectedModel);
}
public ICommand DoubleClickCommand
{
get
{
return _doubleClickCommand;
}
}
How to get the value from the first column (employee_id) and pass it into EmployeeDetails view
For your DataGrid you may use collection of EmployeeDetailsViewModel as ItemsSource. If you do so, selected item will be passed to your command Execute method as an instance of EmployeeDetailsViewModel, and you'll be able to get Id from there.
It looks like you're missing a needed element to show the selected view. If you look at the linked sample note the ItemsControl is contained within a Border which is in turn inside a DockPanel.
Below the DockPanel there is a ContentControl which is a key element needed to show the selected view.