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.
Related
I trying to implement StateContainer by Patrick McCurley in my .NET MAUI application.
It works correctly when the ListView displayed for the first time.
But ListView is not displaying when state changes again until I swipe the screen.
When I add any view element (label, button, etc.) to the view that contains the ListView, it does not show up. But ListView displayed correctly when I move StateContainer to Grid with any other view elements. ListView does not display correctly if the Grid contains no other elements than the StateContainer.
I can't figure out what's the problem here. Grid with other view elements is not a solution for me, because my page should not contain any other elements whan the StateContainer.
Here is an example that reproduces the problem:
P.S. I'm sorry for a lot of code :) I don't know where the problem could be.
States.cs
public enum States
{
Loading,
Success
}
StateCondition.cs
[ContentProperty("Content")]
public class StateCondition : View
{
public object State { get; set; }
public View Content { get; set; }
}
StateContainer.cs
[ContentProperty("Conditions")]
public class StateContainer : ContentView
{
public List<StateCondition> Conditions { get; set; } = new();
public static readonly BindableProperty StateProperty =
BindableProperty.Create(nameof(State), typeof(object), typeof(StateContainer), null, BindingMode.Default, null, StateChanged);
private static void StateChanged(BindableObject bindable, object oldValue, object newValue)
{
var parent = bindable as StateContainer;
if (parent != null)
parent.ChooseStateProperty(newValue);
}
public object State
{
get { return GetValue(StateProperty); }
set { SetValue(StateProperty, value); }
}
private void ChooseStateProperty(object newValue)
{
if (Conditions == null && Conditions?.Count == 0) return;
var stateCondition = Conditions
.FirstOrDefault(condition =>
condition.State != null &&
condition.State.ToString().Equals(newValue.ToString()));
if (stateCondition == null) return;
Content = stateCondition.Content;
}
}
MainPage.xaml
<ContentPage ...>
<state:StateContainer State="{Binding State}">
<state:StateCondition State="Loading">
<StackLayout HorizontalOptions="Center" VerticalOptions="Center">
<ActivityIndicator IsRunning="True" />
<Label Text="Updating data..." />
</StackLayout>
</state:StateCondition>
<state:StateCondition State="Success">
<ListView ItemsSource="{Binding SomeData}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding . }" />
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</state:StateCondition>
</state:StateContainer>
</ContentPage>
MainPage.xaml.cs
public partial class MainPage : ContentPage
{
private States _state;
private int[] _someData;
public MainPage()
{
InitializeComponent();
this.BindingContext = this;
SomeData = new[] { 1, 2, 3, 4, 5 };
State = States.Success;
// it can be executed from outside the page
_ = Task.Run(ExecuteSomeWorkAsync);
}
public States State
{
get => _state;
private set
{
if (_state != value)
{
_state = value;
OnPropertyChanged();
}
}
}
public int[] SomeData
{
get => _someData;
private set
{
if (_someData != value)
{
_someData = value;
OnPropertyChanged();
}
}
}
public async Task ExecuteSomeWorkAsync()
{
await Task.Delay(2000);
State = States.Loading;
await Task.Delay(2000);
// generate new data for displaying
Random rnd = new();
var data = Enumerable.Range(0, 5).Select(n => rnd.Next(0, 5)).ToArray();
SomeData = data;
State = States.Success;
}
}
I suspect Content = stateCondition.Content; won't update display correctly.
As an alternative solution, define public class StateContainer : StackLayout, and use IsVisible="True"/"False" on each child, to control what is shown. All the stateConditions continue to be children of stateContainer, but make only one visible at a time.
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";
}
}
I am working on an iOS App, written in C#, Xamarin. I use a Picker in MVVM Architecture and want to change the pickers title.
but when i change the Pickers Title with OnAddMaterial, the Title doesnt change.
The ViewModel:
private void OnAddNewMaterial()
{
SelectedMaterialIndex = -1;
MaterialPickerTitle = "New Material";
}
private string _materialPickerTitle { get; set; }
public string MaterialPickerTitle
{
get { return _materialPickerTitle; }
set
{
_materialPickerTitle = value;
OnPropertyChanged();
}
}
The View:
<Picker Title="{Binding MaterialPickerTitle}" Margin="12,4,4,4" Grid.Row="0" Grid.Column="0" HorizontalOptions="FillAndExpand" ItemsSource="{Binding Materials}" ItemDisplayBinding="{Binding Name}" SelectedItem="{Binding SelectedMaterial}" SelectedIndex="{Binding SelectedMaterialIndex}" />
I use Visual Studio 2019.
EDIT:
when i am initializing the view, i set the title from the Picker. that works great. After that, i am assigning Objects to the ItemSource from Picker. When i am trying to set the pickers title after that it doesnt works.
I wrote a demo and the title of Picker can be changed after I change the selectedItem. Here is the code you can refer:
public partial class MainPage : ContentPage
{
List<string> monkeyList = new List<string>();
TestModel model = new TestModel();
public MainPage()
{
InitializeComponent();
monkeyList.Add("Baboon");
monkeyList.Add("Capuchin Monkey");
monkeyList.Add("Blue Monkey");
monkeyList.Add("Squirrel Monkey");
monkeyList.Add("Golden Lion Tamarin");
monkeyList.Add("Howler Monkey");
monkeyList.Add("Japanese Macaque");
picker.ItemsSource = monkeyList;
model.MaterialPickerTitle = "123";
model.SelectedMaterialIndex = 2;
BindingContext = model;
}
private void Button_Clicked(object sender, EventArgs e)
{
monkeyList.Add("Baboonww");
model.SelectedMaterialIndex = -1;
model.MaterialPickerTitle = "456";
}
}
class TestModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public TestModel()
{
}
string materialPickerTitle;
public string MaterialPickerTitle
{
set
{
if (materialPickerTitle != value)
{
materialPickerTitle = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("MaterialPickerTitle"));
}
}
}
get
{
return materialPickerTitle;
}
}
int selectedMaterialIndex;
public int SelectedMaterialIndex
{
set
{
if (selectedMaterialIndex != value)
{
selectedMaterialIndex = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("SelectedMaterialIndex"));
}
}
}
get
{
return selectedMaterialIndex;
}
}
}
And in xaml:
<StackLayout>
<!-- Place new controls here -->
<Button Clicked="Button_Clicked" Text="click to change title"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand" />
<Picker x:Name="picker"
Title="{Binding MaterialPickerTitle}"
SelectedIndex="{Binding SelectedMaterialIndex}"
TitleColor="Red">
</Picker>
</StackLayout>
Please check your bindings in your project. Add some breakPoints to debug if the title changes. I also upload my sample project here.
So I started learning xamarin and trying different approaches about data binding from the view to the model. I'm retrieving data from a service using post request and after I get the data I can't bind them to the view. After a lot of research I found some interesting solutions and I tried different approaches.
This what I have achieved until now :
My view :
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Ideas.Pages.IdeasSinglePage"
xmlns:vm="clr-namespace:Ideas.ViewModel;assembly=Ideas"
Title="My idea">
<ContentPage.BindingContext>
<vm:IdeasViewModel/>
</ContentPage.BindingContext>
<StackLayout>
<Button Command="{Binding GetIdeasCommand}"
Text="Bileta Ime"
TextColor="White"
FontSize="15"
BackgroundColor="#29abe2"/>
<Label Text="test"></Label>
<ListView ItemsSource="{Binding Ideas}"
HasUnevenRows="True">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Padding="20, 10">
<Label Text="{Binding IdeasName}"
FontSize="16"
TextColor="RoyalBlue"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
This is my viewmodel
public class IdeasViewModel : INotifyPropertyChanged
{
ApiServices _apiServices = new ApiServices();
public List<Ideas> Ideas
{
get { return Ideas; }
set
{
Ideas= value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
public ICommand GetIdeasCommand
{
get
{
return new Command(async () =>
{
Ideas= await _apiServices.GetIdeasAsync();
});
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
This is my service :
public async Task<List<Ideas>> GetIdeasAsync()
{
ListIdeasDetails ideas= null;
try {
var client = new HttpClient();
client.DefaultRequestHeaders.Add("parameter", "parameter");
client.DefaultRequestHeaders.Add("parameter", parameter);
HttpContent content = new StringContent("");
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
var response = await client.PostAsync("https://heregoes/themethod", content);
response.EnsureSuccessStatusCode();
string json = await response.Content.ReadAsStringAsync();
ideas= JsonConvert.DeserializeObject<ListIdeasDetails>(json);
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message.ToString());
}
return ideas.Ideas;
}
}
And here are my two models :
public class Ideas
{
public string IdeasID { get; set; }
public string IdeasName { get; set; }
}
class ListIdeasDetails
{
public List<Ideas> Ideas{ get; set; }
public string ExceptionMessage { get; set; }
public bool HasException { get; set; }
}
I would really appreciate some help! Thanks!!
Seems like you have a problem with your property definition:
public List<Ideas> Ideas
{
get { return Ideas; }
set
{
Ideas= value; // Endless loop
OnPropertyChanged();
}
}
Add a backing field this way:
List<Ideas> _ideas;
public List<Ideas> Ideas
{
get { return _ideas; }
set
{
if(value == _ideas) return;
_ideas = value;
OnPropertyChanged();
}
}
I would recommend to use Fody.PropertyChanged in order to reduce the amount of boiler-plate code related to INotifyPropertyChanged. Using Fody your ViewModel will look as simple as:
public class IdeasViewModel : INotifyPropertyChanged
{
ApiServices _apiServices = new ApiServices();
public List<Ideas> Ideas { get;set; }
public event PropertyChangedEventHandler PropertyChanged;
public ICommand GetIdeasCommand
{
get
{
return new Command(async () =>
{
Ideas= await _apiServices.GetIdeasAsync();
});
}
}
}
P.S.: Beside your main question I would like to point out the next things that does not look good in your code.
Using POST to download data from WEB.
Class name 'ApiServices'.
You can let me know if you need further assistance by commenting.
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