I am new to Xamarin and I am using ActivityIndicator to say users that app is downloading the data. Problem is that I am using MVVM pattern and I need to set the values IsRunning and IsVisible from ViewModel. I have simple view:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:HmtMobile"
x:Class="HmtMobile.MainPage"
Title="Přihlášení"
>
<ScrollView>
<StackLayout Margin="10" Spacing="15" VerticalOptions="Center">
<ActivityIndicator x:Name="ActivityIndicator" Color="Green" IsRunning="{Binding IsBusy, Mode=TwoWay}" IsVisible="{Binding IsBusy, Mode=TwoWay}"/>
<Entry Placeholder="Uživatelské jméno" Text="{Binding UserName}"></Entry>
<Entry Placeholder="Heslo" Text="{Binding Password}" IsPassword="True"></Entry>
<Button Text="Přihlášení" Command="{Binding LoginCommand}" BackgroundColor="#77D065" TextColor="White"></Button>
</StackLayout>
</ScrollView>
In the view constructor I am assigning the BindingContext
public MainPage()
{
InitializeComponent();
this.BindingContext = new CredentialViewModel(this);
}
Other properties works because when I want to login the properties returns the actual username and password but the twoway binding doesn´t. Properties are declared same so I do not understand why is this happening.
private string password;
public string Password
{
get { return password; }
set { password = value; OnPropertyChanged(); }
}
private bool isBusy;
public bool IsBusy
{
get { return isBusy; }
set { isBusy = value; OnPropertyChanged(); }
}
I am using the simple Login method.
private async Task Login()
{
IsBusy = true;
await Task.Delay(5000);
}
THe ActivityIndicator is not appearing. Does anyone know why? OnPropertyCHanged is coded this way:
public class Bindable
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
I figure it out based on Felix message. The problem was that in Bindable class I forgot to add : INotifyPropertyChanged.
Related
I have a Xamarin project for Android and UWP. This issue seems to only happen on UWP.
In my Xamarin project I have ContentPage with a view model bound as context. In this ViewModel there's an ObservableCollection with another kind of view model. When I create a new instance of this underlying ViewModel and add to my ObservableCollection, sometimes the ContentPage works as expected, showing an item in my ListView. But sometimes there's an empty element added, that I can see when hovering over the list. When this happens I get a bunch of warnings in the Output tab.
My DownloadsPage:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Downloader.Converters"
mc:Ignorable="d"
x:Class="Downloader.Views.DownloadsPage">
<ContentPage.Resources>
<ResourceDictionary>
<local:DownloadStatusToColorConverter x:Key="downloadStatusToColor" />
</ResourceDictionary>
</ContentPage.Resources>
<RefreshView IsRefreshing="{Binding IsBusy, Mode=TwoWay}" Command="{Binding LoadItemsCommand}">
<ListView x:Name="DownloadsListView" SelectionMode="None" ItemsSource="{Binding Downloads}" RowHeight="70">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Padding="10" BackgroundColor="{Binding DownloadStatus, Converter={StaticResource downloadStatusToColor}}">
<Label Text="{Binding Name}"
d:Text="{Binding .}"
LineBreakMode="NoWrap"
Style="{DynamicResource ListItemTextStyle}"
FontSize="16" />
<Grid Grid.Row="0" Grid.Column="0" Padding="10,0,10,0">
<ProgressBar BackgroundColor="Transparent" Progress="{Binding PercentDownloaded}" HorizontalOptions="FillAndExpand" HeightRequest="20">
</ProgressBar>
<Label Text="{Binding PercentString}" HorizontalTextAlignment="Center"></Label>
</Grid>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</RefreshView>
</ContentPage>
DownloadsViewModel is set as context in the code-behind like this:
public partial class DownloadsPage : ContentPage
{
private readonly DownloadsViewModel _viewModel;
public DownloadsPage()
{
InitializeComponent();
BindingContext = _viewModel = new DownloadsViewModel();
Device.StartTimer(TimeSpan.FromSeconds(1), () =>
{
Device.BeginInvokeOnMainThread(() => _viewModel.RefreshDownloads());
return true;
});
}
}
The bound DownloadsViewModel:
public class DownloadsViewModel : BaseViewModel
{
public ObservableCollection<DownloadViewModel> Downloads { get; set; } = new ObservableCollection<DownloadViewModel>();
public Command LoadItemsCommand { get; set; }
public DownloadsViewModel()
{
Title = "Downloads";
LoadItemsCommand = new Command(() => {
IsBusy = true;
Downloads.Clear();
RefreshDownloads();
IsBusy = false;
});
}
public void RefreshDownloads()
{
foreach (var download in DownloadManager.GetDownloads())
{
var existingDownload = Downloads.FirstOrDefault(d => d.Id == download.Id);
if (existingDownload != null)
{
existingDownload.UpdateValues(download);
}
else
{
Downloads.Add(new DownloadViewModel(download));
}
}
}
}
And the ObservableCollection contains DownloadViewModel that looks like this:
public class DownloadViewModel : BaseViewModel
{
private IDownload _download;
public DownloadViewModel(IDownload download)
{
UpdateValues(download);
}
private string _id;
public string Id
{
get { return _id; }
set { SetProperty(ref _id, value); }
}
private string _name;
public string Name
{
get { return _name; }
set { SetProperty(ref _name, value); }
}
private DownloadStatus _status;
public DownloadStatus DownloadStatus
{
get { return _status; }
set { SetProperty(ref _status, value); }
}
public double PercentDownloaded
{
get
{
return _download.DownloadedBytes == -1
? 0f
: (double)_download.DownloadedBytes / _download.TotalBytes;
}
}
public string PercentString { get => $"{(int)(PercentDownloaded * 100)} %"; }
public void UpdateValues(IDownload download)
{
_download = download;
Id = _download.Id;
Name = _download.Name;
DownloadStatus = _download.Status;
}
}
The error I sometimes get which causes items in my ListView to be empty:
Binding: 'DownloadStatus' property not found on 'Downloader.ViewModels.DownloadsViewModel', target property: 'Xamarin.Forms.StackLayout.BackgroundColor'
Binding: 'Name' property not found on 'Downloader.ViewModels.DownloadsViewModel', target property: 'Xamarin.Forms.Label.Text'
Binding: 'PercentDownloaded' property not found on 'Downloader.ViewModels.DownloadsViewModel', target property: 'Xamarin.Forms.ProgressBar.Progress'
Binding: 'PercentString' property not found on 'Downloader.ViewModels.DownloadsViewModel', target property: 'Xamarin.Forms.Label.Text'
When debugging I've confirmed that the item is added to my ObservableCollection as expcted.
How come sometimes it's looking for DownloadStatus, Name, PercentDownloaded and PercentString on DownloadsViewModel instead of DownloadViewModel?
Xamarin UWP seems to bind to the wrong view model
I checked your code sample and it works as expect. But I found the progress value does not update automatically that cause the listview item can't display, I have update the IDownload interface add PercentDownloaded property. For the testing it could works in uwp platform.
The problem was that the ViewModels did not have setters with INotifyPropertyChanged implemented for all properties. The source code is available on Github, and the commit that fixes the issue is this one.
So I've recently just re-worked my larger app project structure to MVVM, however, I'm trying to do simple label binding and I cannot get anything to work at all. I'm probably just being somewhat stupid and missing an obvious thing here but I cannot figure this out. I'm not currently trying to do this in an ObservableCollection because I can't even get the basics to work currently. Just a simple Label.
My TaskModel class contains
public class TaskModel
{
public string Title { get; set; }
}
My MainViewModel contains
public class MainViewModel
{
TaskModel task = new TaskModel
{
Title = "Hello"
};
}
My MainPage Xaml contains
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Testing"
x:Class="Testing.MainPage"
xmlns:bc="clr-namespace:Testing.ViewModels">
<ContentPage.Padding>
<OnPlatform x:TypeArguments="Thickness"
iOS="0, 20, 0, 0"
Android="0, 40, 0, 0">
</OnPlatform>
</ContentPage.Padding>
<StackLayout BindingContext="{x:Reference Slider}" HorizontalOptions="Center" VerticalOptions="Center">
<BoxView Color="Green" Opacity="{Binding Value}"></BoxView>
<Label BindingContext="{x:Reference Slider}" Text="{Binding
Value,
StringFormat='Value is {0:F2}'}"
Opacity="{Binding Value}">
</Label>
<Slider x:Name="Slider" ></Slider>
<Label Text="{Binding TaskModel.Hello}"></Label>
</StackLayout>
And then my MainPage.Xaml.CS contains
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
BindingContext = new MainViewModel();
}
}
So as you can see, nothing too special. Feeling a bit stupid that I can't work this out and any assistance on this matter would be greatly appreciated!
EDIT:
Ignore the extra XAML, the XAML above the label at the base is just me testing other binding within the same XAML page, which works, just trying to get it to bind outside of the page if you get what I'm saying.
Remove BindingContext from the following xaml, since context spread to control's children. Hence you change Label's context to slider control. Binding will not work if it points to incorrect context.
<StackLayout BindingContext="{x:Reference Slider}"
Bindings does not work with private properties and you have private field in your ViewModel. Implement INPC:
public class MainViewModel: INotifyPropertyChanged
{
MainViewModel()
{
_task = new TaskModel()
{
Title = "Hello"
};
}
TaskModel _task;
public TaskModel TaskModel
{
get=>_task;
set
{
if (value!=_task)
{
_task = value;
NotifyPropertyChanged(nameof(this.TaskModel));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
<Label Text="{Binding TaskModel.Title}"></Label>
Implement INPC in TaskModel
public class TaskModel : INotifyPropertyChanged
{
public string Title
{
get=>_title;
set=>
{
if (value!=_title)
{
value=title;
OnPropertyChanged(nameof(Title));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
For more, please visit docs.
Change the 2nd last line of your xaml: Binding TaskModel.Hello to Binding TaskModel.Title.
If this won't fix it, implement INPC in your TaskModel class and call it in the setter of the Title property
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
I genuinely can't work out if I'm being an idiot or not, what I'm to do is so simple!
I have a ViewModel with a public Username field.
Originally I tried to get the Entry to display an initial value but upon looking further I've discovered I can't even get the view to update the viewmodel.
I want the text to reflect the entered username when I click the button and display it in the Alert.
My ViewModel:
public class LoginViewModel
{
public String Username { get; set; }
public String Password { get; set; }
public int UserId { get; set; }
}
My Page:
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage Padding="15,15,15,15"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
x:Class="EngineerApp.LoginPage">
<AbsoluteLayout>
<StackLayout VerticalOptions="EndAndExpand" Orientation="Vertical">
<Image Source="traklogo.png" VerticalOptions="FillAndExpand"></Image>
<Label TextColor="#207cad" Text="Username" />
<Label Text="{Binding Username}"></Label>
<Entry Text="{Binding Username, Mode=TwoWay}" TextColor="Black" Placeholder="Please enter your username..." x:Name="txtUsername" />
</StackLayout>
</AbsoluteLayout>
</ContentPage>
My Page's Code:
public partial class LoginPage : ContentPage
{
public LoginViewModel viewModel = new LoginViewModel();
public LoginPage()
{
InitializeComponent();
BindingContext = viewModel;
btnLogIn.Clicked += delegate
{
DisplayAlert("Test",viewModel.Username,"Okay");
return;
};
}
}
This working snippet from a LoginViewModel that implements INotifyPropertyChanged is in an app published in stores:
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
public string Username
{
get { return username; }
set
{
if (username == value) return;
username = value?.ToLowerInvariant();
OnPropertyChanged();
}
}
And the associated Entry XAML snippet:
<Entry x:Name="UsernameEntry" HeightRequest="45" Keyboard="Email"
Placeholder="{Binding LabelUsername}" Text="{Binding Username}">
I've worked it out. The page BEFORE this page was passing in it's own BindingContext. Removing this resolved my issues.
Using Visual Studio 2015 proff,
My LoginViewModel Class (Portable Class Library)
public class LoginViewModel : INotifyPropertyChanged, INotifyCollectionChanged
{
LoginPage page;
private ObservableCollection<Employees> _employeeList;
private string _loginName;
public ObservableCollection<Employees> EmployeeList
{
get { return _employeeList; }
set
{
_employeeList = value;
OnPropertyChanged();
OnCollectionChanged(NotifyCollectionChangedAction.Reset);
}
}
public string LoginName
{
get { return _loginName; }
set
{
_loginName = value;
if (_loginName != null)
{
OnPropertyChanged();
}
}
}
public LoginViewModel(LoginPage parent)
{
page = parent;
}
public async void GetEmployees()
{
var loginService = new LoginService();
EmployeeList = await loginService.GetEmployeesAsync();
}
public event PropertyChangedEventHandler PropertyChanged;
public event NotifyCollectionChangedEventHandler CollectionChanged;
protected virtual void OnCollectionChanged( NotifyCollectionChangedAction action)
{
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(action));
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
My LoginPage.xaml (Portable Class Library)
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="ScannerApp.Views.LoginPage"
xmlns:ViewModels="clr-namespace:ScannerApp.ViewModels;assembly=ScannerApp">
<StackLayout Orientation="Vertical">
<Label Text="Please Login"
VerticalOptions="Start"
HorizontalTextAlignment="Center"
IsVisible="true"
FontSize="Large"
FontAttributes="Bold" />
<ListView x:Name="mylist" ItemsSource="{Binding EmployeeList}"
HasUnevenRows="True" SelectedItem="{Binding LoginName}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Vertical" Padding="12,6">
<Label Text="{Binding Name}"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
My LoginPage.xaml.cs Class (Portable Class Library)
public partial class LoginPage : ContentPage
{
public LoginPage()
{
InitializeComponent();
BindingContext = new LoginViewModel(this);
}
protected override void OnAppearing()
{
base.OnAppearing();
LoginViewModel model = new LoginViewModel(this);
model.GetEmployees();
BindingContext = model;
}
public ListView MyList
{
get
{
return mylist;
}
}
}
Question
I get a list of employees, the list on the front end renders this. The user then Selects a name from the list, At this point I would like to detect this and then navigate to my different page.
Currently my property is not hit, I'm wondering if this has anything to do with my Binding on the code behind "OnAppearing"? but I'm not sure.
While what you have may work there are a few tweaks I would suggest.
No need to set your BindingContext in your constructor and in OnAppearing(). Just make your LoginViewModel a class level private property in your code-behind and only assign it to your BindingContext in your constructor. Then call GetEmployees() in OnAppearing().
Also, you should make GetEmployees() return a Task, in order to await as far up the chain as possible.
ViewModel:
....
public async Task GetEmployees()
{
var loginService = new LoginService();
EmployeeList = await loginService.GetEmployeesAsync();
}
....
Code-behind:
public partial class LoginPage : ContentPage
{
private LoginViewModel _model;
public LoginPage()
{
InitializeComponent();
BindingContext = _model = new LoginViewModel(this);
}
protected override async void OnAppearing() //Notice I changed this to async and can now await the GetEmployees() call
{
base.OnAppearing();
await _model.GetEmployees();
}
public ListView MyList
{
get
{
return mylist;
}
}
private async void OnItemSelected(object sender, SelectedItemChangedEventArgs e) {
if (e.SelectedItem == null) return;
await Navigation.PushAsync(new MenuPage());
}
}
XAML:
<!-- Adding OnItemSelected in XAML below -->
<ListView x:Name="mylist"
ItemsSource="{Binding EmployeeList}"
HasUnevenRows="True"
SelectedItem="{Binding LoginName}"
ItemSelected="OnItemSelected">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Vertical" Padding="12,6">
<Label Text="{Binding Name}"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
mylist.ItemSelected += async (sender, e) => {
if (e.SelectedItem == null) return;
await Navigation.PushAsync(new MenuPage());
};
This works, had to set the page to a nav wrap in the App.cs, then apply this event handler to the OnAppearing method.