SeeMore Functionality for Xamarin Forms - xamarin

I am developing a xamarin forms project. I need to a Label with in a listview which by default display max of 2 lines and then shows elipses(...) also I want to add a readmore button which enable the user to view the truncated text. Again the text need to be truncated when button is clicked. It is like "See more" & "See less" functionality. I have made it display 3 by using max lines property. Please anyone suggest me how to achieve this. I have added the screenshot for further reference.
This is my XAML For the label.
<Label Text="{Binding Note, Mode=TwoWay}" Padding="0,0,0,0" MaxLines="3"
LineBreakMode="TailTruncation" Style="{StaticResource TabContentLabelStyle}"
IsVisible="{Binding IsEdit,Converter={StaticResource BoolConverter}}" />
This is how it should look when Collapsed:

Made a quick sample based on what #anand said
Create custom control called CustomLabel
CustomLabel.xaml
<ContentView
x:Class="BlankApp3.Controls.CustomLabel"
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:prism="http://prismlibrary.com"
mc:Ignorable="d">
<ContentView.Content>
<StackLayout HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
<Label x:Name="customLabel" />
<Label
x:Name="lblReadMore"
FontSize="18"
TextColor="#1a0fa9">
<Label.GestureRecognizers>
<TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped" />
</Label.GestureRecognizers>
</Label>
</StackLayout>
</ContentView.Content>
</ContentView>
Code behind
using System;
using System.Diagnostics;
using System.Linq;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace BlankApp3.Controls
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class CustomLabel : ContentView
{
public CustomLabel()
{
InitializeComponent();
}
#region Bindable Property
public static readonly BindableProperty TextProperty = BindableProperty.Create(
propertyName: nameof(TextProperty),
returnType: typeof(string),
declaringType: typeof(CustomLabel),
defaultBindingMode: BindingMode.TwoWay,
propertyChanged: TextPropertyChanged
);
public string Text
{
get { return (string)base.GetValue(TextProperty); }
set { base.SetValue(TextProperty, value); }
}
private static void TextPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (CustomLabel)bindable;
if (newValue != null)
{
control.customLabel.Text = (string)newValue;
var ss = control.customLabel.Text.Split().Length;
if (control.customLabel.Text.Split().Length >= 30)
{
control.ShortTextVisible = true;
control.ReadMoreLabel = true;
}
}
}
#endregion Bindable Property
public bool ReadMoreLabel { get; set; }
private bool _shortTextVisible;
public bool ShortTextVisible
{
get => _shortTextVisible;
set { _shortTextVisible = value; ShortTextPropertyChanged(); }
}
private void ShortTextPropertyChanged()
{
if (Text != null && Text.Split().Length >= 30)
{
if (ShortTextVisible)
{
if (customLabel != null && !string.IsNullOrWhiteSpace(customLabel.Text) && customLabel.Text.Split().Length < 100)
{
Debug.WriteLine("");
}
customLabel.Text = string.Join(" ", Text.Split().Take(30));
lblReadMore.Text = "See more";
lblReadMore.IsVisible = true;
}
else
{
customLabel.Text = Text;
lblReadMore.Text = "See less";
}
}
}
private void TapGestureRecognizer_Tapped(object sender, EventArgs e)
{
ShortTextVisible = !ShortTextVisible;
}
}
}
MainPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
x:Class="BlankApp3.Views.MainPage"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:BlankApp3.Controls"
Title="{Binding Title}"
BackgroundColor="#ffffff">
<CollectionView ItemsSource="{Binding Monkeys}">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout>
<StackLayout Padding="10" Orientation="Horizontal">
<Image
HeightRequest="80"
Source="https://d2gg9evh47fn9z.cloudfront.net/800px_COLOURBOX13204546.jpg"
VerticalOptions="Start" />
<StackLayout HorizontalOptions="FillAndExpand">
<Frame
Margin="10,0,10,0"
BackgroundColor="#f9f9f9"
CornerRadius="10"
HasShadow="False">
<StackLayout>
<Label FontAttributes="Bold" Text="Dr. Gracy David" />
<local:CustomLabel Text="{Binding .}" />
</StackLayout>
</Frame>
<StackLayout Margin="10,0,10,0" Orientation="Horizontal">
<Label Text="12 April,2020" />
<Label Text="3.20pm" />
</StackLayout>
</StackLayout>
</StackLayout>
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ContentPage>
MainPageViewModal.cs
using Prism.Navigation;
using System.Collections.ObjectModel;
namespace BlankApp3.ViewModels
{
public class MainPageViewModel : ViewModelBase
{
public ObservableCollection<string> Monkeys { get; set; }
public MainPageViewModel(INavigationService navigationService)
: base(navigationService)
{
Title = "Main Page";
Monkeys = new ObservableCollection<string>();
}
public async override void OnNavigatedTo(INavigationParameters parameters)
{
Monkeys = new ObservableCollection<string>()
{
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
"Simple",
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."
};
}
}
}

You could use FormattedText, as in this example:
https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/text/label#formatted-text
and use the command on the last Span(make it look like a hyper link) to expand / collapse the text.
The first Span's Text property can bind to viewmodel's text property that will have part of the full text, and upon command execute will have the full text.

Related

MAUI MVVM Architecture, doing a login system while consuming a API

I am trying to learn MAUI in order to create a project, but it seems like i got stuck. I cant understand the MVVM architecture, as i never had any simillar experience before. I will now present my code, and would love to get answers that could explain why it is not working, and a possible solution for the problem.
I have three folders: Views, where i store the design. Models, where i store the classes. And ViewModels, that get the data. This is the xaml content page, which consists of a login page.
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="thebridgeproject.Views.login"
xmlns:ViewModels="clr-namespace:thebridgeproject.ViewModels"
Shell.NavBarIsVisible="False"
Title="LoginPage" >
<ContentPage.BindingContext>
<ViewModels:LoginViewModel />
</ContentPage.BindingContext>
<VerticalStackLayout
Spacing="25"
Padding="30,0"
VerticalOptions="Center">
<Image Source="loginicon.png" HeightRequest="150" WidthRequest="150" />
<VerticalStackLayout Spacing="5">
<Label Text="Welcome!" FontSize="28" TextColor="#3B7A5E" HorizontalTextAlignment="Center" />
<Label Text="Login to your account" FontSize="18" TextColor="Gray" HorizontalTextAlignment="Center" />
</VerticalStackLayout>
<StackLayout Orientation="Horizontal">
<Frame ZIndex="1" HasShadow="True" BorderColor="White" HeightRequest="56" WidthRequest="56" CornerRadius="28">
<Image Source="user.png" HeightRequest="20" WidthRequest="20" />
</Frame>
<Frame HeightRequest="45" Margin="-20,0,0,0" Padding="0" HasShadow="True" BorderColor="White" HorizontalOptions="FillAndExpand">
<Entry Text="{Binding Username}" Margin="20,0,0,0" VerticalOptions="Center" Placeholder="Username"/>
</Frame>
</StackLayout>
<StackLayout Orientation="Horizontal">
<Frame ZIndex="1" HasShadow="True" BorderColor="White" HeightRequest="56" WidthRequest="56" CornerRadius="28">
<Image Source="lock.png" HeightRequest="20" WidthRequest="20" />
</Frame>
<Frame HeightRequest="45" Margin="-20,0,0,0" Padding="0" HasShadow="True" BorderColor="White" HorizontalOptions="FillAndExpand">
<Entry Text="{Binding Password}" Margin="20,0,0,0" VerticalOptions="Center" Placeholder="Password" IsPassword="True" />
</Frame>
</StackLayout>
<Button Text="Sign in" WidthRequest="100" CornerRadius="20" HorizontalOptions="Center" BackgroundColor="#3B7A5E" Command="{Binding LoginCommand}" />
<StackLayout Orientation="Horizontal" Spacing="5" HorizontalOptions="Center">
<Label Text="Dont have an account?" TextColor="Gray" />
<Label Text="Sign up here" TextColor="#50b3f2" />
</StackLayout>
</VerticalStackLayout>
</ContentPage>
Then i have the Model that holds the data for the API request.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace thebridgeproject.Models
{
class users
{
public class Result
{
public int NumUtente { get; set; }
public string Nome { get; set; }
public string Password { get; set; }
public string Morada { get; set; }
public string Cidade { get; set; }
public string DataNascimento { get; set; }
public string NumTlf { get; set; }
}
public class Root
{
public bool success { get; set; }
public string message { get; set; }
public List<Result> result { get; set; }
}
}
}
After that, we have the LoginViewModel:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using thebridgeproject.Models;
namespace thebridgeproject.ViewModels
{
public class LoginViewModel : INotifyPropertyChanged
{
private string _username;
public string Username
{
get { return _username; }
set
{
_username = value;
OnPropertyChanged(nameof(Username));
}
}
private string _password;
public string Password
{
get { return _password; }
set
{
_password = value;
OnPropertyChanged(nameof(Password));
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private async Task Login()
{
using (var httpClient = new HttpClient())
{
httpClient.DefaultRequestHeaders.Add("Authorization", "RaV9N");
var response = await httpClient.GetAsync("http:///bp/utentes");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var users = JsonConvert.DeserializeObject<users.Root>(content);
if (users.success)
{
var user = users.result.FirstOrDefault(x => x.Nome == Username);
if (user != null && VerifyPassword(user.Password, Password))
{
// Login successful
// ...
}
else
{
// Login failed
// ...
}
}
else
{
// API request failed
// ...
}
}
else
{
// API request failed
// ...
}
}
}
private bool VerifyPassword(string hashedPassword, string enteredPassword)
{
// Use the BCrypt.Net library to verify the entered password
return BCrypt.Net.BCrypt.Verify(enteredPassword, hashedPassword);
}
}
}
Ignore the API link! But that is mostly it, i have no more code. It seems like it does nothing. I think the issue might be the lack of code in the file behind the design. Im open to suggestions, and i am thankfull for any productive answer!
The view model implements properties and commands to which the view can data bind to, and notifies the view of any state changes through change notification events. The view model is also responsible for coordinating the view's interactions with any model classes that are required. There's typically a one-to-many relationship between the view model and the model classes. You can refer to The Model-View-ViewModel Pattern for more details.
You can refer to my sample code below on how to use LoginCommand in your LoginCommand. Notice that I put it in a Label TapGestureRecognizer, however its usage is the same as that you use it in a Button.
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:ViewModel="clr-namespace:MyApp.ViewModels"
xmlns:local="clr-namespace:MyApp"
x:Class="MyApp.Views.LoginPage"
BackgroundColor="#112B47"
>
<ContentPage.BindingContext>
<ViewModel:LoginViewModel/>
</ContentPage.BindingContext>
<StackLayout Padding="15" VerticalOptions="Center" HorizontalOptions="FillAndExpand">
<Label HorizontalOptions="Center">
<Label.FormattedText>
<FormattedString>
<Span Text="Don't have an account?" TextColor="Gray"></Span>
<Span Text="Register" TextColor="Gray" FontAttributes="Bold" TextDecorations="Underline"></Span>
</FormattedString>
</Label.FormattedText>
<Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding LoginCommand}"></TapGestureRecognizer>
</Label.GestureRecognizers>
</Label>
</StackLayout>
</ContentPage>
LoginViewModel:
public class LoginViewModel : INotifyPropertyChanged
{
public ICommand LoginCommand { get; private set; }
public event PropertyChangedEventHandler PropertyChanged;
public LoginViewModel()
{
LoginCommand = new Command(async () => await Login());
}
public async Task Login()
{
// add your logic here
}
}

.NET MAUI CollectionView dont get populated

I made a very simple .NET MAUI App based on the ClickMe Code thats generated by VS2022.
The "ClickMe" Button should add a entry to a CollectionView which is binded to a ObservableCollection, but it don't populate the view if click the button although "monkeys" are added to the ObservableCollection maybe somebody can help what I'm missing.
public class Monkey
{
public string Name { get;set; }
public Monkey(string name) {Name = name; }
}
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MauiObservableList.MainPage">
<ScrollView>
<VerticalStackLayout Spacing="25" Padding="30,0" >
<Button x:Name="CounterBtn" Text="Click me" Clicked="OnCounterClicked" HorizontalOptions="Center" />
<CollectionView ItemsSource="{Binding Monkeys}">
<CollectionView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Name}" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</VerticalStackLayout>
</ScrollView>
</ContentPage>
MainPage.xaml.cs:
int count = 0;
public ObservableCollection<Monkey> Monkeys { get; set; } = new ObservableCollection<Monkey>();
public MainPage()
{
InitializeComponent();
BindingContext = this;
}
private void OnCounterClicked(object sender, EventArgs e)
{
count++;
if (count == 1)
CounterBtn.Text = $"Clicked {count} time";
else
CounterBtn.Text = $"Clicked {count} times";
Monkeys.Add(new Monkey(CounterBtn.Text));
}
Replace TextCell with a Label. TextCell only works with ListView

How to open a page from a class?

I am trying to create a little application using MVVM. I would like to open a second page that scan a QR code and then back the value of the QR code to the main page.
I have this code:
Main view:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="QRScannerXamarinForms.Views.MainPageView"
xmlns:zxing="clr-namespace:ZXing.Net.Mobile.Forms;assembly=ZXing.Net.Mobile.Forms"
xmlns:vm="clr-namespace:QRScannerXamarinForms.ViewModels">
<ContentPage.BindingContext>
<vm:MainPageViewModel/>
</ContentPage.BindingContext>
<StackLayout>
<Frame BackgroundColor="#2196F3" Padding="24" CornerRadius="0">
<Label Text="Barcode Sample" HorizontalTextAlignment="Center" TextColor="White" FontSize="36" />
</Frame>
<Button x:Name="ucBtnEscanear" Text="Escanear"
Command="{Binding EscanearCommand}"/>
</StackLayout>
</ContentPage>
Main view model:
private INavigationService _navigationService;
public ICommand EscanearCommand { get; private set; }
private void Escanear()
{
string miCodigoQr = _navigationService.AbrirPaginaEscaner();
}
Second view:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="QRScannerXamarinForms.Views.ScannerView"
xmlns:zxing="clr-namespace:ZXing.Net.Mobile.Forms;assembly=ZXing.Net.Mobile.Forms"
xmlns:vm="clr-namespace:QRScannerXamarinForms.ViewModels">
<ContentPage.BindingContext>
<vm:ScannerViewModel/>
</ContentPage.BindingContext>
<ContentPage.Content>
<StackLayout>
<Frame BackgroundColor="#2196F3" Padding="24" CornerRadius="0">
<Label Text="Barcode Sample" HorizontalTextAlignment="Center" TextColor="White" FontSize="36" />
</Frame>
<Label x:Name="ucScanResultText" />
<zxing:ZXingScannerView x:Name="ucZXingScannerView" IsScanning="True" IsAnalyzing="True" Result="{Binding CodigoQr}" />
</StackLayout>
</ContentPage.Content>
</ContentPage>
Second view model:
class ScannerViewModel : BaseViewModel
{
private string _codigoQr;
public string CodigoQr
{
get { return _codigoQr; }
set { _codigoQr = value; }
}
}
Interface to open pages:
namespace QRScannerXamarinForms.Services
{
interface INavigationService
{
string AbrirPaginaEscaner();
}
}
Implementation of the interface that allows navigation.
namespace QRScannerXamarinForms.Services
{
class ViewNavigationService : INavigationService
{
public string AbrirPaginaEscaner()
{
ScannerView miScannerView = new ScannerView();
ScannerViewModel miScannerViewModel = miScannerView.BindingContext as ScannerViewModel;
Application.Current.MainPage.Navigation.PushModalAsync(miScannerView);
return miScannerViewModel.CodigoQr;
}
}
}
But in the implementation of the interface, the second page is not shown. So this line doesn't work:
Application.Current.MainPage.Navigation.PushModalAsync(miScannerView);
How could I open from this class?
Thanks.

How to implement a command to delete an item from my database in xamarinforms mvvm style

Currently i have a clicked function that delete an items :
async void DeleteButtonClicked(object sender, EventArgs e)
{
ImageButton button = sender as ImageButton;
var agenda = button.BindingContext as Agenda;
await App.Database.DeleteAgendaAsync(agenda);
await Navigation.PopAsync();
}
I want to implement this MVVM style with a command, the basic of my app is that i have an AgendaPage that loads items in a collection view, right now i want to be able to call the command on this icon and so it delete the item.
Thanks for your help.
Currently in my PageViewModel i only have this
I have to pass the current agenda to the DeleteAgendaAsync() method but it's unclear to me where to get it.
public Command DeleteAgendaCommand { get; set; }
public AgendaPageViewModel()
{
DeleteAgendaCommand = new Command(async () => await DeleteAgenda());
}
async Task DeleteAgenda()
{
await App.Database.DeleteAgendaAsync();
}
AgendaDatabase.cs in the Database folder
using System;
using System.Collections.Generic;
using System.Text;
using SQLite;
using Calculette.Models;
using System.Threading.Tasks;
using Calculette.ViewModel;
namespace Calculette.Database
{
public class AgendaDatabase
{
readonly SQLiteAsyncConnection database;
public AgendaDatabase(string dbPath)
{
database = new SQLiteAsyncConnection(dbPath);
database.CreateTableAsync<Agenda>().Wait();
}
// Get all agenda
public Task<List<Agenda>> GetAgendasAsync()
{
return database.Table<Agenda>().ToListAsync();
}
// Get specific agenda
public Task<Agenda> GetAgendaAsync(int id)
{
return database.Table<Agenda>()
.Where(i => i.ID == id)
.FirstOrDefaultAsync();
}
// Insert new agenda (save)
public Task<int> SaveAgendaAsync(Agenda agenda)
{
if (agenda.ID != 0)
{
return database.UpdateAsync(agenda);
}
else
{
return database.InsertAsync(agenda);
}
}
//Delete specific agenda
public Task<int> DeleteAgendaAsync(Agenda agenda)
{
return database.DeleteAsync(agenda);
}
public Task<int> AddAgendaAsync(Agenda agenda)
{
return database.InsertAsync(agenda);
}
}
}
Agenda.cs in the Models folder
[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; }
This is the NewFormViewModel.cs which is used to create new agenda items to the collectionview, it feels like i would need to access all the items added there, but im unsure on how to do that for the DeleteCommand
using Calculette.Database;
using Calculette.Models;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
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()
{
// Initialise la valeur du DatePicker a celle d'aujourd'hui
this.Date = DateTime.Now;
// Commande pour la sauvegarde sur la page NewFormPage ( voir SaveForm() plus bas)
AgendaSaveFormCommand = new Command(async () => await SaveForm(), () => !IsBusy);
}
// Création des propriétés d'un agenda
private string topic;
public string Topic
{
get => topic;
set
{
topic = value;
NotifyPropertyChanged();
}
}
private string duration;
public string Duration
{
get => duration;
set
{
duration = value;
NotifyPropertyChanged();
}
}
private DateTime date;
public DateTime Date
{
get => date;
set
{
date = value;
NotifyPropertyChanged();
}
}
bool isBusy = false;
public bool IsBusy
{
get { return isBusy; }
set
{
isBusy = value;
NotifyPropertyChanged();
AgendaSaveFormCommand.ChangeCanExecute();
}
}
public int ID { get; }
// Methode qui enregistre un agenda et l'ajoute a la collection d'agenda de AgendaPage
async Task SaveForm()
{
IsBusy = true;
await Task.Delay(4000);
IsBusy = false;
// Agenda agenda = new Agenda();
//ObservableCollection<Agenda> agenda = new ObservableCollection<Agenda>();
Agenda agenda = new Agenda();
agenda.Topic = Topic;
agenda.Date = Date;
agenda.Duration = Duration;
await App.Database.SaveAgendaAsync(agenda);
await Application.Current.MainPage.DisplayAlert("Save", "La tâche a été enregistrée", "OK");
await Application.Current.MainPage.Navigation.PopAsync();
}
}
AgendaPage.xaml
<CollectionView Grid.Row="2" Margin="25" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
SelectionMode="Single" x:Name="AgendaCollection" ItemsSource="{Binding Agendas}"> <!--ItemsSource="{Binding AngedaCollection}" -->
<CollectionView.Header>
<StackLayout Orientation="Horizontal" Spacing="220">
<Label Text="Agenda" TextColor="Black" FontSize="18"/>
<StackLayout Orientation="Horizontal">
<ImageButton Source="iconplus.png" HeightRequest="30" WidthRequest="30" Clicked="GoToNewFormPage"></ImageButton>
<ImageButton Source="iconmoins.png" HeightRequest="30" WidthRequest="30" Clicked="DeleteButtonClicked"></ImageButton>
</StackLayout>
</StackLayout>
</CollectionView.Header>
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Vertical" ItemSpacing="20"/>
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate >
<DataTemplate>
<pv:PancakeView HasShadow="True" BackgroundColor="White" VerticalOptions="StartAndExpand "
HorizontalOptions="FillAndExpand">
<Grid VerticalOptions="StartAndExpand" HorizontalOptions="FillAndExpand">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<BoxView BackgroundColor="{Binding Color}" WidthRequest="3" HorizontalOptions="Start"
VerticalOptions="FillAndExpand"/>
<Expander Grid.Column="1" >
<Expander.Header>
<Grid HorizontalOptions="FillAndExpand">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="3.5*"/>
</Grid.ColumnDefinitions>
<StackLayout HorizontalOptions="Center" VerticalOptions="Center">
<Label Text="{Binding Date, StringFormat='{0:dd}'}" TextColor="#008A00" FontSize="27"
HorizontalOptions="Center"/>
<Label Text="{Binding Date, StringFormat='{0:MMMM}'}" TextColor="Black" FontSize="10"
HorizontalOptions="Center" Margin="0,-10,0,0" FontAttributes="Bold"/>
<ImageButton Source="iconplus.png" HorizontalOptions="Center" HeightRequest="30" WidthRequest="30" Clicked="GoToFormPage"></ImageButton>
</StackLayout>
<BoxView Grid.Column="1" BackgroundColor="#F2F4F8" WidthRequest="1" HorizontalOptions="Start"
VerticalOptions="FillAndExpand"/>
<StackLayout x:Name="topicLayout" Grid.Column="2" HorizontalOptions="Start" VerticalOptions="Center" Margin="20">
<Label Text="{Binding Topic}" TextColor="#008A00" FontSize="15" FontAttributes="Bold"/>
<Label Text="{Binding Duration}" TextColor="#2F3246" FontSize="12" Margin="0,-10,0,0"/>
<ImageButton Source="iconmoins.png" HeightRequest="30" WidthRequest="30" Command="{Binding Source={x:Reference AgendaCollection}, Path=AgendaPageViewModel.DeleteAgendaCommand}"
CommandParameter="{Binding .}"></ImageButton>
</StackLayout>
</Grid>
</Expander.Header>
<Grid HorizontalOptions="FillAndExpand">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="3.5*"/>
</Grid.ColumnDefinitions>
<BoxView Grid.Column="1" BackgroundColor="#F2F4F8" WidthRequest="1" HorizontalOptions="Start"
VerticalOptions="FillAndExpand"/>
<StackLayout Grid.Column="2" Spacing="10">
<Label Text="Tâches" TextColor="Black" FontSize="15" Margin="20,0"/>
<StackLayout BindableLayout.ItemsSource="{Binding Speakers}" HorizontalOptions="Start" VerticalOptions="Center" Margin="20,0,0,20">
<BindableLayout.ItemTemplate>
<DataTemplate>
<Label TextColor="#2F3246" FontSize="12">
<Label.FormattedText>
<FormattedString>
<FormattedString.Spans>
<Span Text="{Binding Time}"/>
<Span Text=" - "/>
<Span Text="{Binding Name}" FontAttributes="Bold"/>
</FormattedString.Spans>
</FormattedString>
</Label.FormattedText>
</Label>
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
</StackLayout>
</Grid>
</Expander>
</Grid>
</pv:PancakeView>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
Im not sure where you have your delete button, so here are two possible ways of doing this:
1) You select an Agenda from the CollectionView and then use one Button to delete this agenda:
Add the SelectedItem as a Property to the ViewModel like that:
ObservableCollection<Agenda> agendas = new ObservableCollection<Agenda>();
public ObservableCollection<Agenda> Agendas { get => agendas; set => agendas = value; }
public Agenda SelectedItem { get; set; }
public Command Delete { get; set; }
public ViewModel()
{
Agendas.Add(new Agenda() { Name = "test" });
Agendas.Add(new Agenda() { Name = "test2" });
Agendas.Add(new Agenda() { Name = "test3" });
Delete = new Command(new Action<object>((obj) =>
{
DeleteAsync(SelectedItem);
}));
}
private void DeleteAsync(Agenda selectedItem)
{
//delete Agenda here
}
To use this simply bind the SelectedItem property of the CollectionView to your ViewModel:
<StackLayout>
<StackLayout.BindingContext>
<local:ViewModel/>
</StackLayout.BindingContext>
<CollectionView ItemsSource="{Binding Agendas}"
SelectionMode="Single"
SelectedItem="{Binding SelectedItem}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Frame>
<StackLayout>
<Label Text="{Binding Name}" FontSize="120" Margin="12"/>
</StackLayout>
</Frame>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<Button Text="Delete" Command="{Binding Delete}" />
</StackLayout>
2) You have a button to delete an Agenda for each Agenda:
You can setup your ViewModel similar to this:
ObservableCollection<Agenda> agendas = new ObservableCollection<Agenda>();
public ObservableCollection<Agenda> Agendas { get => agendas; set => agendas = value; }
public Command Delete { get; set; }
public ViewModel()
{
Delete = new Command(new Action<object>((obj) =>
{
DeleteAsync((Agenda) obj);
}));
}
private void DeleteAsync(Agenda selectedItem)
{
//delete Agenda here
}
Then you can set this up in xaml like this:
<StackLayout>
<StackLayout.BindingContext>
<local:ViewModel/>
</StackLayout.BindingContext>
<CollectionView x:Name="AgendaCollection"
ItemsSource="{Binding Agendas}" >
<CollectionView.ItemTemplate>
<DataTemplate>
<Frame>
<StackLayout>
<Label Text="{Binding Topic}" FontSize="120" Margin="12"/>
<Button Text="Delete"
Command="{Binding Source={x:Reference AgendaCollection}, Path=BindingContext.Delete}"
CommandParameter="{Binding .}"/>
</StackLayout>
</Frame>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</StackLayout>
Note you can directly Bind to a Command, you dont have to use ButtonClicked and than pass it on.
In this case you have to change the Binding Source to the CollectionView (give that a x:Name and Bind to that) because the Binding Context inside the DataTemplate will be set to the Children of the ItemSource (the individual Agendas). That way the command is automatically executed without the Clicked event.
To pass on the Agenda on which delete was clicked to the Command you can set the CommandParameter. Here it Binds to itself (As i said CollectionView will set the DataContext of its elements to the corresponding individual element.)

How to use Xamarin.Forms MediaManager library with MVVM to play youtube videos

I'm trying to get working the MediaManager library playing a couple videos from youtube using MVVM approach.
My idea is to have a single view where initially is loaded a video, once the user watched the first loaded video he can click a button to see another video in the same view.
I can't find many examples on the internet related about how to accomplish this using MVVM, every example I've found uses the codebehind approach but my app is created in full MVVM so I need to get it working like that.
This is what I've done by now.
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:behaviors="clr-namespace:Behaviors;assembly=Behaviors"
xmlns:mediamanager="clr-namespace:Plugin.MediaManager.Forms;assembly=Plugin.MediaManager.Forms"
x:Class="VideoView"
BindingContext="{Binding VideoViewModel, Source={StaticResource ServiceLocator}}">
<ContentPage.Behaviors>
<behaviors:EventHandlerBehavior EventName="Appearing">
<behaviors:InvokeCommandAction Command="{Binding PageAppearingCommand}" />
</behaviors:EventHandlerBehavior>
</ContentPage.Behaviors>
<StackLayout Margin="10,60,10,0">
<Label x:Name="LblMsg"/>
<Grid HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
<mediamanager:VideoView x:Name="TrainingVideoPlayer"
AspectMode="AspectFill"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand" />
<StackLayout VerticalOptions="End" HorizontalOptions="FillAndExpand">
<ProgressBar x:Name="progress" HeightRequest="10" Progress="{Binding ProgressStatus, Mode=TwoWay}" />
<StackLayout Orientation="Horizontal" HorizontalOptions="CenterAndExpand" Spacing="10" VerticalOptions="End">
<Image x:Name="ImgPlay"
Source="video_play.png"
HorizontalOptions="Center">
<Image.GestureRecognizers>
<TapGestureRecognizer Command="{Binding PlayTappedCommand}" NumberOfTapsRequired="1" />
</Image.GestureRecognizers>
</Image>
<Image x:Name="ImgPause"
Source="video_pause.png"
HorizontalOptions="Center">
<Image.GestureRecognizers>
<TapGestureRecognizer Command="{Binding PauseTappedCommand}" NumberOfTapsRequired="1" />
</Image.GestureRecognizers>
</Image>
<Image x:Name="ImgStop"
Source="video_stop.png"
HorizontalOptions="Center">
<Image.GestureRecognizers>
<TapGestureRecognizer Command="{Binding StopTappedCommand}" NumberOfTapsRequired="1" />
</Image.GestureRecognizers>
</Image>
</StackLayout>
</StackLayout>
</Grid>
<Button x:Name="BtnNext" Command="{Binding NextCommand}"/>
</StackLayout>
</ContentPage>
ViewModel
public class VideoViewModel : AppBaseViewModel
{
//Commands
public ICommand PageAppearingCommand { get; set; }
public ICommand PlayTappedCommand { get; set; }
public ICommand PauseTappedCommand { get; set; }
public ICommand StopTappedCommand { get; set; }
public ICommand NextCommand { get; set; }
//Fields
private double _progressStatus;
private string youtubevideourl;
//Properties
public double ProgressStatus
{
get => _progressStatus;
set
{
if (Set(ref _progressStatus, value))
{
RaisePropertyChanged(() => ProgressStatus);
}
}
}
public VideoViewModel()
{
PageAppearingCommand = new Command(OnPageAppearing);
PlayTappedCommand = new Command(OnImgPlay_Tapped);
PauseTappedCommand = new Command(OnImgPause_Tapped);
StopTappedCommand = new Command(OnImgStop_Tapped);
NextCommand = new Command(OnBtnNext_Click);
youtubevideourl = "https://urloftheyoutubevideo";
}
private async void OnPageAppearing()
{
await CrossMediaManager.Current.Stop();
//Sets the videoplayer events to control playback status
CrossMediaManager.Current.PlayingChanged += VideoPlayer_PlayingChanged;
CrossMediaManager.Current.MediaFinished += VideoPlayer_MediaFinished;
}
private async void OnImgPlay_Tapped()
{
await CrossMediaManager.Current.Play(youtubevideourl, MediaFileType.Video);
}
private async void OnImgPause_Tapped()
{
await CrossMediaManager.Current.Pause();
}
private async void OnImgStop_Tapped()
{
await CrossMediaManager.Current.Stop();
}
private void VideoPlayer_PlayingChanged(object sender, PlayingChangedEventArgs e)
{
Device.BeginInvokeOnMainThread(() =>
{
ProgressStatus = e.Progress;
});
}
private void VideoPlayer_MediaFinished(object sender, MediaFinishedEventArgs e)
{
//Some logic
}
private async void OnBtnNext_Click()
{
//logic to load the next video url
}
}
Using this code I can execute play/pause/stop methods of the MediaManager but nothing happens in the View, I cant even see 1 second of the youtube video.
I will appreciate your help
Note: I installed the MediaManager nuget package in my three projects: Android, iOS and NetStandard library
Note2: One of my requirements is to guarantee as much as possible the user watched the whole video before moving to the next one (the value will be stored in an external DB), so any solution different to this one must follow this requirement

Resources