- Xamarin
- Xamarin.Forms
In this tutorial, we will discover how to download a file with Xamarin.Forms and follow the download status through a progress bar.
First of all, setup a Xamarin.Forms solution like this :
|-XFDownloadProject
|-XFDownloadProject
|-XFDownloadProject.Forms
|-XFDownloadProject.Android
|-XFDownloadProject.iOS
|-XFDownloadProject.UWP
The structure is composed by one .Net Standard library for the shared Code (XFDownloadProject
), another one for the Xamarin.Forms
views (XFDownloadProject.Forms
) and the three platforms projects : iOS, Android and UWP.
The idea behind this structure, is to be able to use our XFDownloadProject
library in other Xamarin.iOS, Xamarin.Android or UWP projects.
Add the XFDownloadProject.Forms
project as a dependency of all platforms projects.
For this tutorial we will use one of my favorite nuget package : MvvmLightLibs
, for dependency injection, and RelayCommand.
So, let’s install it in all projects, here is the link : MvvmLightLibsStd10
It’s time to setup our download service, first create an interface called : IDownloadService
and add this method :
public interface IDownloadService
{
Task DownloadFileAsync(string url, IProgress<double> progress, CancellationToken token);
}
Let’s implement this interface in a DownloadService
class :
public class DownloadService : IDownloadService
{
private HttpClient _client;
private readonly IFileService _fileService;
public DownloadService(IFileService fileService)
{
_client = new HttpClient();
_fileService = fileService;
}
public async Task DownloadFileAsync(string url, IProgress<double> progress, CancellationToken token)
{
}
}
As you can see above, we setup the HttpClient
that we will use to download a file, and a IFileService
which I will explain in more detail later.
Now let’s implement our DownloadFileAsync
method :
private int bufferSize = 4095;
public async Task DownloadFileAsync(string url, IProgress<double> progress, CancellationToken token)
{
try
{
// Step 1 : Get call
var response = await _client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
if (!response.IsSuccessStatusCode)
{
throw new Exception(string.Format("The request returned with HTTP status code {0}", response.StatusCode));
}
// Step 2 : Filename
var fileName = response.Content.Headers?.ContentDisposition?.FileName ?? "tmp.zip";
// Step 3 : Get total of data
var totalData = response.Content.Headers.ContentLength.GetValueOrDefault(-1L);
var canSendProgress = totalData != -1L && progress != null;
// Step 4 : Get total of data
var filePath = Path.Combine(_fileService.GetStorageFolderPath(), fileName);
// Step 5 : Download data
using (var fileStream = _fileService.OpenStream(filePath))
{
using (var stream = await response.Content.ReadAsStreamAsync())
{
var totalRead = 0L;
var buffer = new byte[bufferSize];
var isMoreDataToRead = true;
do
{
token.ThrowIfCancellationRequested();
var read = await stream.ReadAsync(buffer, 0, buffer.Length, token);
if (read == 0)
{
isMoreDataToRead = false;
}
else
{
// Write data on disk.
await fileStream.WriteAsync(buffer, 0, read);
totalRead += read;
if (canSendProgress)
{
progress.Report((totalRead * 1d) / (totalData * 1d) * 100);
}
}
} while (isMoreDataToRead);
}
}
}
catch (Exception e)
{
// Manage the exception as you need here.
System.Diagnostics.Debug.WriteLine(e.ToString());
}
}
Based on the steps number in the code above we have :
Step 1 :
We make the HTTP GET call to the server, and we are waiting for a success status code in the HTTP Header
to start the download.
Step 2 :
We define the name of our content that we are downloading. If you have a nice API you will have the filename in the HTTP Header
,
like the zip file we will download from one of my previous post on Github :
Content-Disposition → attachment; filename=XamarinAndroidParcelable-master.zip
If no Content-Disposition
is returned by the API we will use tmp.zip
as a default filename.
Step 3 :
We get the Length of the file we will download, also using Content-Disposition
.
Content-Length → 56159
Step 4 :
Get the path of our file and destination folder. To achieve that create an interface in our shared code called IFileService
and add this method :
public interface IFileService
{
String GetStorageFolderPath();
}
We will implement this interface in each platform to save our data. So create a FileService
class in our Xamarin.iOS, Xamarin.Android and UWP project,
that inherit from IFileService
:
Here is the implementation for Xamarin.iOS :
public string GetStorageFolderPath()
{
return Environment.GetFolderPath(Environment.SpecialFolder.Personal);
}
For Xamarin.Android :
public string GetStorageFolderPath()
{
string docFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
string libFolder = Path.Combine(docFolder, "..", "Library");
return libFolder;
}
And finally for UWP :
public string GetStorageFolderPath()
{
return Windows.Storage.ApplicationData.Current.LocalFolder.Path;
}
Step 5 :
Last step, open a FileStream
and save our data in it. To do that, add this method to our DownloadService
:
private Stream OpenStream(string path)
{
return new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, bufferSize);
}
We are expecting more data and report the progress with the progress
parameter that will be used to communicate the progress of the download to our view.
Let’s create a ViewModel called DownloadViewModel
for our application. We will create a command called StartDownloadCommand
using RelayCommand
from MvvmLightLibs
. This command will execute the StartDownloadAsync
method :
private double _progressValue;
public double ProgressValue
{
get { return _progressValue; }
set { Set(ref _progressValue, value); }
}
private bool _isDownloading;
public bool IsDownloading
{
get { return _isDownloading; }
set { Set(ref _isDownloading, value); }
}
private readonly IDownloadService _downloadService;
public ICommand StartDownloadCommand { get; }
public DownloadViewModel(IDownloadService downloadService)
{
_downloadService = downloadService;
StartDownloadCommand = new RelayCommand(async () => await StartDownloadAsync());
}
public async Task StartDownloadAsync()
{
var progressIndicator = new Progress<double>(ReportProgress);
var cts = new CancellationTokenSource();
try
{
IsDownloading = true;
var url = "https://github.com/damienaicheh/XamarinAndroidParcelable/archive/master.zip";
await _downloadService.DownloadFileAsync(url, progressIndicator, cts.Token);
}
catch (OperationCanceledException ex)
{
System.Diagnostics.Debug.WriteLine(ex.ToString());
//Manage cancellation here
}
finally
{
IsDownloading = false;
}
}
internal void ReportProgress(double value)
{
ProgressValue = value;
}
The ReportProgress
method will refresh the progress value to display it in the view.
In the XFDownloadProject.Forms
project create a ContentPage and add this view :
<?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:XFDownloadProject"
x:Class="XFDownloadProject.MainPage"
xmlns:converters="using:XFDownloadProject.Converters">
<ContentPage.Resources>
<converters:InverterBooleanConverter x:Key="InverterBooleanConverter" />
<converters:ValueProgressBarConverter x:Key="ValueProgressBarConverter" />
</ContentPage.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Label Text="Download a file with a progress bar in Xamarin Forms !" FontSize="Large"
HorizontalOptions="Center" VerticalOptions="Center"
HorizontalTextAlignment="Center"/>
<StackLayout Grid.Row="1" Spacing="20">
<ProgressBar Progress="{Binding ProgressValue, Converter={StaticResource ValueProgressBarConverter}}" Margin="10,0"/>
<Label Text="{Binding ProgressValue, StringFormat='{0:F2}%'}" HorizontalOptions="Center" />
<Button Text="Start Download"
Command="{Binding StartDownloadCommand}"
IsEnabled="{Binding IsDownloading, Converter={StaticResource InverterBooleanConverter}}" />
</StackLayout>
</Grid>
</ContentPage>
You probably notice that there is two converters used, one for inverting the bool
to disable the button during download,
and one for converting the ProgressValue
in the right format for the ProgressBar
.
Here are the two implementations that we will place in a Converters
folder :
InverterBooleanConverter
:
public class InverterBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return !(bool)value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return new NotImplementedException();
}
}
ValueProgressBarConverter
:
public class ValueProgressBarConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return (double)value / 100;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return new NotImplementedException();
}
}
To be able to start the project we need to inject our ViewModels and Services. For that we need to create a Bootstrap
class in the
shared code, and register our classes like this :
public class Bootstrap
{
private static Bootstrap instance;
public static Bootstrap Instance
{
get
{
if (instance == null)
instance = new Bootstrap();
return instance;
}
}
public void Setup()
{
SimpleIoc.Default.Register<IDownloadService, DownloadService>();
SimpleIoc.Default.Register<DownloadViewModel>();
}
}
You have probably noticed that we don’t register the FileService
in the Setup
method. It’s because this is platform specific so we need
to inject it in the different platforms projects :
For Xamarin.iOS in the AppDelegate
class :
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
global::Xamarin.Forms.Forms.Init();
SimpleIoc.Default.Register<IFileService, FileService>();
LoadApplication(new App());
return base.FinishedLaunching(app, options);
}
For Xamarin.Android in the MainActivity
class :
protected override void OnCreate(Bundle bundle)
{
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
base.OnCreate(bundle);
global::Xamarin.Forms.Forms.Init(this, bundle);
SimpleIoc.Default.Register<IFileService, FileService>();
LoadApplication(new App());
}
And finally for UWP in the App.xaml.cs
class :
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
Frame rootFrame = Window.Current.Content as Frame;
// Do not repeat app initialization when the Window already has content,
// just ensure that the window is active
if (rootFrame == null)
{
// Create a Frame to act as the navigation context and navigate to the first page
rootFrame = new Frame();
rootFrame.NavigationFailed += OnNavigationFailed;
Xamarin.Forms.Forms.Init(e);
SimpleIoc.Default.Register<IFileService, FileService>();
//...
}
Then, in our App.xaml
from our XFDownloadProject.Forms
project we need to call our Setup
method :
public partial class App : Application
{
public App()
{
InitializeComponent();
// MainPage must be not null for UWP.
MainPage = new ContentPage();
}
protected override void OnStart()
{
// Handle when our app starts
Bootstrap.Instance.Setup();
MainPage = new MainPage();
}
//...
}
Now go back to the MainPage.xaml.cs
and in the constructor you can add the BindingContext
by getting an instance of the DownloadViewModel
provided
by our dependency injector :
this.BindingContext = SimpleIoc.Default.GetInstance<DownloadViewModel>();
It’s time to run the project and discover what we did !
I advise you to not use an Android Emulator to test it because it probably will not be able to solve the URL correctly and you will get an error like :
System.Net.Http.HttpRequestException: An error occurred while sending the request —> System.Net.WebException: Error: NameResolutionFailure
So if you can, use a physical device. If everything is ok you will see something like this :
You will find full source code in this Github repository.
Happy coding!
You liked this tutorial? Leave a star in the associated Github repository!