How to setup my bindingcontext for my xamarin.forms app with mvvm and sqlite - xamarin

The basic of my app is that i have an AgendaPage which has a collectionview of items and when i click the add icon, i can fill a form to populate this collectionview.
My app was working when i didn't set my app MVVM style but i am trying to apply the MVVM logic to my app.
Currently if i set the BindingContext of my NewFormPage to Agenda() my NewFormPage opens when i click the add button BUT if i set my BindingContext to NewFormViewModel, nothing open up and my app crash so i am trying to figure out what i am doing wrong in setting up this MVVM.
Note: currently i only have a clicked function to open up the page ( didn't implement the command yet, i was trying to implement the save command).
Agenda.cs in the database folder
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using SQLite;
using Calculette.Database;
namespace Calculette.Models
{
[Table("Agenda")]
public class Agenda
{
[PrimaryKey, AutoIncrement]
public int ID { get; set; }
public string Topic { get; set; }
public string Duration { get; set; }
public DateTime Date { get; set; }
}
}
NewFormViewModel.cs in the ViewModel folder
using Calculette.Database;
using Calculette.Models;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using Xamarin.Forms;
namespace Calculette.ViewModel
{
class NewFormViewModel : BaseViewModel
{
public Command AgendaSaveFormCommand { get; set; }
public NewFormViewModel()
{
AgendaSaveFormCommand = new Command(async () => await SaveForm(), () => !IsBusy);
}
public string Topic
{
get => Topic;
set
{
Topic = value;
NotifyPropertyChanged();
}
}
public string Duration
{
get => Duration;
set
{
Duration = value;
NotifyPropertyChanged();
}
}
public DateTime Date
{
get => Date;
set
{
Date = value;
NotifyPropertyChanged();
}
}
bool isBusy = false;
public bool IsBusy
{
get { return isBusy; }
set
{
isBusy = value;
//OnPropertyChanged();
AgendaSaveFormCommand.ChangeCanExecute();
}
}
public int ID { get; }
async Task SaveForm()
{
IsBusy = true;
await Task.Delay(4000);
IsBusy = false;
Agenda agenda = new Agenda();
await App.Database.SaveAgendaAsync(agenda);
await Application.Current.MainPage.DisplayAlert("Save", "La tâche a été enregistrée", "OK");
}
}
}
NewFormPage.xaml.cs in the Views folder
using Calculette.Models;
using Calculette.ViewModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace Calculette.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class NewFormPage : ContentPage
{
public NewFormPage ()
{
InitializeComponent ();
// BindingContext = new Agenda();
BindingContext = new NewFormViewModel();
}
}
}
AgendaPage.xaml in the views folder (xaml that calls Clicked that open NewFormPage
<ImageButton Source="iconplus.png" HeightRequest="30" WidthRequest="30" Clicked="GoToNewFormPage"></ImageButton>
the GoToNewFormPage function in AgendaPage.xaml.cs
protected async void GoToNewFormPage(object sender, EventArgs e)
{
await Navigation.PushAsync(new Views.NewFormPage());
}

You did not set value for the object's property when clicking on the add icon.
async Task SaveForm()
{
IsBusy = true;
await Task.Delay(4000);
IsBusy = false;
Agenda agenda = new Agenda();
//here you should set value of a blank object
Agenda.Topic = xx;
Agenda.Date = xx;
Agenda.Duration = xx;
await App.Database.SaveAgendaAsync(agenda);
await Application.Current.MainPage.DisplayAlert("Save", "La tâche a été enregistrée", "OK");
}

all of your properties are implemented incorrectly
this will lead to an infinite recursive loop - Duration will call Duration will call Duration... until your app crashes
public string Duration
{
// this will just call the capital-D Duration property again
// which will then call itself again, and again, and again
get => Duration;
set
{
// this will call the setter for the capital-D Duration property recursively
Duration = value;
NotifyPropertyChanged();
}
}
you need to have a private backing field that is named differently than the property
private string duration;
public string Duration
{
get => duration;
set
{
duration = value;
NotifyPropertyChanged();
}
}

Related

Binding text label on a home page to a timer

We have a really simple app, the idea is the timer will update a label on the home screen depending on different configuration within the mobile app. I have created the binding and can update the homepage from it's self but not from the timer. I think what is missing is a OnChange within the home page to detect if the string has changed.
Display layout code, bind the label to the name "LabelText"
<Label
Text = "{Binding LabelText, Mode=TwoWay}"
x:Name="MainPageStatusText"
HorizontalOptions="CenterAndExpand"
Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="6"
VerticalOptions="CenterAndExpand"
TextColor="White"
FontSize="Medium"/>
This is the class file to link the text string to the label, I can see it been called from the different places but when it's called from the app.cs it does not work
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Xamarin.Forms;
namespace Binding_Demo
{
public class MyClass : INotifyPropertyChanged
{
protected void OnPropertyChanged(PropertyChangedEventArgs e)
{ PropertyChanged?.Invoke(this, e); }
protected void OnPropertyChanged(string propertyName)
{ OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); }
public event PropertyChangedEventHandler PropertyChanged;
private string labelText;
public string LabelText
{
get {
return labelText;
}
set
{
labelText = value;
OnPropertyChanged("LabelText");
}
}
}
}
This is the code inside the homepage, this works and I can see it sending data to the text label
public static MyClass _myClass = new MyClass();
public Homepage()
{
BindingContext = _myClass;
_myClass.LabelText = "Inside the home page";
}
This is the App.cs code, we start the timer and then want to set the text on the Homepage label. I can see the class been called, but it does not set the text.
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace Binding_Demo
{
public partial class App : Application
{
public static MyClass _myClass = new MyClass();
public App()
{
//InitializeComponent();
Device.StartTimer(TimeSpan.FromSeconds(10), () =>
{
Task.Run(() =>
{
Debug.WriteLine("Timer has been triggered");
// !!!!! This is not setting the text in the label !!!!!
BindingContext = _myClass;
_myClass.LabelText = "Inside the timer app";
});
return true; //use this to run continuously
});
MainPage = new NavigationPage(new MainPage());
}
protected override void OnStart()
{
//
}
protected override void OnSleep()
{
}
protected override void OnResume()
{
// force app to mainpage and clear the token
}
}
}
I have created the binding and can update the homepage from it's self but not from the timer.
As Jason said, please make sure the binding model is unique. You could create a global static instance of MyClass in App class, then bind this instance to HomePage.
Check the code:
App.xaml.cs
public partial class App : Application
{
public static MyClass _myClass = new MyClass();
public App()
{
InitializeComponent();
Device.StartTimer(TimeSpan.FromSeconds(5), () =>
{
Task.Run(() =>
{
_myClass.LabelText = "Inside the timer app";
});
return true;
});
MainPage = new NavigationPage(new Homepage());
}
}
Homepage.xaml.cs:
public Homepage()
{
InitializeComponent();
BindingContext = App._myClass;
}

Xamarin, why can't my if/else statement, retrieving data from Settings.plugin

LastPickName is not empty but it kept picking the else statement... maybe I'm putting it in the wrong area?
I have a label text to output what's in LastPickName just to make sure it's not empty or empty.
using Plugin.Settings;
using Plugin.Settings.Abstractions;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
namespace App424
{
// by visiting https://aka.ms/xamarinforms-previewer
[DesignTimeVisible(false)]
public partial class MainPage : ContentPage
{
private static ISettings AppSettings => CrossSettings.Current;
public static string LastPickValue
{
get => AppSettings.GetValueOrDefault(nameof(LastPickValue), string.Empty);
set => AppSettings.AddOrUpdateValue(nameof(LastPickValue), value);
}
public static string LastPickName
{
get => AppSettings.GetValueOrDefault(nameof(LastPickName), string.Empty);
set => AppSettings.AddOrUpdateValue(nameof(LastPickName), value);
}
public object LastPickname { get; private set; }
public MainPage()
{
InitializeComponent();
List<string> list = new List<string>();
list.Add("Right1");
list.Add("Right2");
list.Add("Right3");
list.Add("Right4");
//populate picker selection
drainlocationPicker1.ItemsSource = list;
//Set the default value
drainlocationPicker1.SelectedItem = LastPickValue;
nameEntry.Placeholder = LastPickName;
//names.Text = LastPickName;
nameLabel.Text = LastPickName;
if (LastPickName == null)
{
settingsButton.IsVisible = true;
nextButton.IsVisible = false;
}
else
{
nextButton.IsVisible = true;
settingsButton.IsVisible = false;
}
void Handle_Unfocused(object sender, Xamarin.Forms.FocusEventArgs e)
{
LastPickName = nameEntry.Text;
}
private void DrainlocationPicker1_SelectedIndexChanged(object sender, EventArgs e)
{
string nlocation1 = (string)drainlocationPicker1.SelectedItem;
LastPickValue = nlocation1;
}
}
}
}
LastPickName is empty it should show settingsButton if not show nextButton.
LastPickName is empty why is it not showing settingsButton?
if (LastPickName == "")
i thought null is empty but as Sami commented it's not.

Xamarin MVVM passing data to other view

I want to pass the data to another view page. So far I can get the data I need to pass. My problem is how do I pass the data in MVVM. I used Application.Current.MainPage.Navigation.PushAsync(new DatabaseSyncPage(), true); When I add contactId inside DatabaseSyncPage() an error occurs. "The error is 'DatabaseSyncPage' does not contain a constructor that takes 1 arguments"
My code:
LoginPageViewModel.cs
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Net;
using System.Text;
using System.Windows.Input;
using TBSMobileApplication.Data;
using TBSMobileApplication.View;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace TBSMobileApplication.ViewModel
{
public class LoginPageViewModel : INotifyPropertyChanged
{
void OnProperyChanged(string PropertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropertyName));
}
public string username;
public string password;
public string Username
{
get { return username; }
set
{
username = value;
OnProperyChanged(nameof(Username));
}
}
public string Password
{
get { return password; }
set
{
password = value;
OnProperyChanged(nameof(Password));
}
}
public class LoggedInUser
{
public string ContactID { get; set; }
}
public ICommand LoginCommand { get; set; }
public LoginPageViewModel()
{
LoginCommand = new Command(OnLogin);
}
public void OnLogin()
{
if (string.IsNullOrEmpty(Username) || string.IsNullOrEmpty(Password))
{
MessagingCenter.Send(this, "Login Alert", Username);
}
else
{
var current = Connectivity.NetworkAccess;
if (current == NetworkAccess.Internet)
{
var link = "http://192.168.1.25:7777/TBS/test.php?User=" + Username + "&Password=" + Password;
var request = HttpWebRequest.Create(string.Format(#link));
request.ContentType = "application/json";
request.Method = "GET";
using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
{
if (response.StatusCode != HttpStatusCode.OK)
{
Console.Out.WriteLine("Error fetching data. Server returned status code: {0}", response.StatusCode);
}
else
{
using (StreamReader reader = new StreamReader(response.GetResponseStream()))
{
var content = reader.ReadToEnd();
if (content.Equals("[]") || string.IsNullOrWhiteSpace(content) || string.IsNullOrEmpty(content))
{
MessagingCenter.Send(this, "Http", Username);
}
else
{
var result = JsonConvert.DeserializeObject<List<LoggedInUser>>(content);
var contactId = result[0].ContactID;
Application.Current.MainPage.Navigation.PushAsync(new DatabaseSyncPage { myId = contactId }, true);
}
}
}
}
else
{
MessagingCenter.Send(this, "Not Connected", Username);
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
DatabaseSyncPage.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace TBSMobileApplication.View
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class DatabaseSyncPage : ContentPage
{
public int myId { get; set; }
public DatabaseSyncPage ()
{
InitializeComponent ();
DisplayAlert("Message", Convert.ToString(myId), "ok");
}
}
}
If you want to send the int. First declare that in your DatabaseSyncPage
Like below
public partial class DatabaseSyncPage : ContentPage
{
public DatabaseSyncPage( int Id)
{
}
}
& when you are pushing your page in your code else block do like this
if (content.Equals("[]") || string.IsNullOrWhiteSpace(content) || string.IsNullOrEmpty(content))
{
MessagingCenter.Send(this, "Http", Username);
}
else
{
var result = JsonConvert.DeserializeObject<List<LoggedInUser>>(content);
var contactId = result[0].ContactID;
Application.Current.MainPage.Navigation.PushAsync(new DatabaseSyncPage(contactId), true);
}
I'm assuming that contactID is an int.
Create an additional constructor in your DatabaseSyncPage:
public DatabaseSyncPage (int contactID)
{
// TODO: Do something with your id
}
But this passes the data to the page, not the page model.
Are you using any kind of framework? It would probably be worth looking into that.
You can use xamrin.plugins.settings nuget package.

How to Databind in Xamarin PCL project?

I want to make a simple login UI in xaml using Xamarin. I create a username and password field with Entry in the MainPage and then I try to bind them to my LoginViewModel where I can access my connexion method.
When I define the Binding context in the Mainpage codebehind the application simply shutdown and I dont understand why, what am I doing wrong ?
MainPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:suivAAndroid"
x:Class="suivAAndroid.MainPage">
<StackLayout
VerticalOptions="CenterAndExpand">
<Image></Image>
<Label
Text="Login"
StyleId="lbl_login"></Label>
<Entry
StyleId="ent_login"
Text="{Binding Username}"></Entry>
<Label
Text="Mot de passe"
StyleId="ent_mdp"></Label>
<Entry
StyleId="ent_mdp"
Text="{Binding Password}"></Entry>
<Button
Clicked="connexion_click"
Text="Connexion"></Button>
</StackLayout>
</ContentPage>
MainPage.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace suivAAndroid
{
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
BindingContext = new LoginViewModel(); // Here is where it does not work. If the line is commented out, then the application launch without stopping but because there is no binding context I cant get the user inputs.
}
private void connexion_click(object sender, EventArgs e)
{
LoginViewModel connexionBtn = new LoginViewModel();
Device.BeginInvokeOnMainThread(async () =>
{
await connexionBtn.Connexion();
});
}
}
}
LoginViewModel.cs
using suivAAndroid.Models;
using suivAAndroid.Views;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace suivAAndroid
{
public class LoginViewModel
{
#region propriétés
public string Username
{
get
{
return Username;
}
set
{
Username = value;
}
}
public string Password
{
get
{
return Password;
}
set
{
Password = value;
}
}
#endregion
#region constructor
public LoginViewModel()
{
}
#endregion
#region methodes
public void CreerListeVisiteurDur(List<Visiteur> uneListe)
{
Visiteur unVisiteur = new Visiteur("Clooney", "George", "cgeorge", "azerty", "rue du port", "59", "lille", new DateTime(2015 / 07 / 13));
uneListe.Add(unVisiteur);
}
public async Task Connexion()
{
List<Visiteur> uneListe = new List<Visiteur>();
CreerListeVisiteurDur(uneListe);
if (!string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(Password))
{
foreach (Visiteur unVisiteur in uneListe)
{
string login = unVisiteur.login;
string pass = unVisiteur.mdp;
if (login == Username && pass == Password)
{
App.Current.MainPage = new CreerVisite();
}
}
}
}
#endregion
}
}
Your ViewModel properties have infinite loops:
public string Username
{
get
{
return Username;
}
set
{
Username = value;
}
}
calling Username = value will call set on Username which in turn calls Username = value again.
Also, in order for your ViewModel to be bindable, you must implement INotifyPropertyChanged.
If you want a framework that is easy to use to help you do this, I would suggest Mvvm Light.
Here's an example of what your ViewModel should look like:
public class MyViewModel : INotifyPropertyChanged
{
public event EventHandler<PropertyChangedEventArgs> OnPropertyChanged;
private string _username;
public string Username
{
get
{
return _username;
}
set
{
_username = value;
PropertyChanged?.Invoke(new PropertyChangedEventArgs("Username");
}
}
....
}
in connexion_click you are creating a new copy of your VM that has no relation to the prior copy you created for your BindingContext.
public partial class MainPage : ContentPage
{
private LoginViewModel vm;
public MainPage()
{
InitializeComponent();
vm = new LoginViewModel();
BindingContext = vm;
}
private void connexion_click(object sender, EventArgs e)
{
Device.BeginInvokeOnMainThread(async () =>
{
await vm.Connexion();
});
}
}
your VM should implement INotifyPropertyChanged
your VM has a recursive getter

Scrolling to start of Xamarin Forms ListView with header

I'm having some trouble scrolling to the top of a ListView in Xamarin Forms. I can scroll to the first item by calling ScrollTo and passing the first item. The problem is that when the list has a header item, I can't find a way to scroll to the header. Is this possible? The only work around I can think of is to not use the header and just have another item at the start of the ItemSource list that acts as a header but I'd rather use the header if possible. Thanks.
So I've solved this myself now. My solution was to subclass ListView and add a public ScrollToTop method which invokes an internal ScrollToTopRequestedEvent when called. I then subclassed the ListViewRenderer on each platform and registered for the event.
In the Android renderer I'm then calling Control.SmoothScrollToPositionFromTop(0, 0) to scroll to top.
In the iOS rendered I'm calling Control.ScrollRectToVisible(new CoreGraphics.CGRect(0, 0, 1, 1), true).
Wah all credits #Gareth Wynn, man that was cool thx.
Anyway here's the code for everyone to use, change class names and namespace, iOS not included, do same as for Android just using Gareth Wynn's hint in parallel answer:
SHARED NiftyListView.cs :
using System;
using Xamarin.Forms;
namespace AppoMobi
{
public class NiftyListView : CListView
{
public event EventHandler EventScrollToTop;
//-------------------------------------------------------------------
public void ScrollToTop(bool animate=true)
//-------------------------------------------------------------------
{
//bool animate is not used at this stage, it's always animated.
EventScrollToTop?.Invoke(this, EventArgs.Empty);
}
}
}
ANDROID NiftyListView.Android.cs :
using System;
using AppoMobi;
using AppoMobi.Droid.Renderers;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using ListView = Xamarin.Forms.ListView;
[assembly: ExportRenderer(typeof(NiftyListView), typeof(NiftyListViewRenderer))]
namespace AppoMobi.Droid.Renderers
{
//-------------------------------------------------------------------
class NiftyListViewRenderer : ListViewRenderer
//-------------------------------------------------------------------
{
protected override void OnElementChanged(ElementChangedEventArgs<ListView> e)
{
base.OnElementChanged(e);
var view = (NiftyListView)Element;
if (view == null) return;
view.EventScrollToTop += OnScrollToTop;
}
//-------------------------------------------------------------------------------------------
public async void OnScrollToTop(object sender, EventArgs eventArgs)
//-------------------------------------------------------------------------------------------
{
Control.SmoothScrollToPositionFromTop(0, 0);
}
}
}
ScrollTo(Object, Object, ScrollToPosition, Boolean)
Scrolls the ListView to the item in the group
https://learn.microsoft.com/en-us/dotnet/api/xamarin.forms.listview.scrollto?view=xamarin-forms
Model for each group:
public class Notification {
public int Id { get; set; }
public string Title { get; set; };
public Notification(int id, string title) {
Id = id;
Title = title;
}
}
Group-Model for ItemSource:
public class NotificationGroup: List<Notification> {
public string Title { get; set; }
public string ShortTitle { get; set; }
public NotificationGroup(string title) {
Title = title;
}
}
Sample Data & Usage:
//SAMPLE DATA
var notifications = new ObservableCollection <NotificationGroup> {
new NotificationGroup("Group-01") {
new Notification(1, "Item-1"),
new Notification(2, "Item-2")
},
new NotificationGroup("Group-02") {
new Notification(3, "Item-3"),
new Notification(4, "Item-4")
}
};
YourListViewName.ItemSource = notifications;
//USING
YourListViewName.ScrollTo(notifications.First()[0], notifications.First(), ScrollToPosition.Start, true);

Resources