Photo by pxhere.com

Download a file with progress bar in 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.

Posted by Damien Aicheh on July 10, 2018 · 21 mins

In this tutorial, we will discover how to download a file with Xamarin.Forms and follow the download status through a progress bar.

Setup the project

Structure

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.

Nuget

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

Services

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.

The ViewModel

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.

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();
    }
}

Dependency injection

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>();

Run our project

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 :

All platforms

You will find full source code in this Github repository.

Happy coding!

You liked this tutorial? Leave a star in the associated Github repository!

Do not hesitate to follow me on to not miss my next tutorial!