Статьи

Создание относительно простого менеджера загрузок в приложении Windows Phone 7

Загрузка внутри приложений Windows Phone 7 не является чем-то необычным. Многие приложения загружают все виды данных все время — контент XML, видео, музыку, изображения — вы называете это. Большинство из них организованы для работы в фоновом режиме, поэтому пользователь не знает, что происходит. Однако в некоторых случаях необходимо показать, как происходит процесс загрузки. Вот где простой менеджер загрузок будет работать отлично.

ПРИМЕЧАНИЕ. Я использую MVVM Light для привязки к базовой модели представления, содержащей данные для загрузки. Вы можете скачать библиотеки MVVM (которые необходимы) здесь .

Так вот как я это сделал.

Прежде всего я создал приложение Silverlight для Windows Phone. На самом деле не имеет значения, какой тип приложения вы используете (если, конечно, вы не решите использовать XNA) — все это вращается вокруг выделенного кода и элемента управления ListBox. В конечном итоге я хотел добиться чего-то вроде этого:

Легко, правда? Давайте посмотрим на базовую архитектуру для чего-то подобного.

  • ModelLocator.cs — вспомогательный класс, используемый для поиска ViewModels внутри моего приложения. Он основан на структуре, описанной в образце по умолчанию, предоставленном Laurent Bugnion для MVVM Light.
  • HomePageVideModel.cs — ViewModel, к которой я привязываюсь. Он объявляет основную коллекцию загрузки, среди некоторых других вспомогательных элементов.
  • Download.cs — класс, который умеет организовывать загрузки и обрабатывать очереди.

Вы, наверное, заметили, что я говорил об очереди здесь. Действительно, нерационально начинать все загрузки сразу, особенно если загруженный контент довольно большой. Вот где вам нужно убедиться, что только ограниченное число одновременных загрузок запущено и запущено.

Вот макет страницы менеджера загрузки, где находится используемый мной ListBox:

<Grid x:Name="LayoutRoot" Background="Transparent">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
            <TextBlock Text="DOWNLOADS" Style="{StaticResource PhoneTextNormalStyle}"/>
        </StackPanel>

        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
            <ListBox x:Name="dlList" ItemsSource="{Binding Downloads}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <Grid>
                            <TextBlock Margin="10,0,20,0" FontSize="24" Text="{Binding Name}"></TextBlock>
                            <ProgressBar Margin="0,40,60,0" Width="400" Style="{StaticResource ProgressBarStyle1}" Value="{Binding Progress}"></ProgressBar>
                        </Grid>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
        </Grid>
    </Grid>

Я использую только TextBlock и ProgressBar в шаблоне элемента. Конечно, это можно «настроить», чтобы показать скорость загрузки и тому подобное. Однако для демонстрационных целей этого будет достаточно. В конце концов, я говорю об очень общей реализации.

Вы можете видеть, что я связываю несколько свойств здесь. Они содержатся в классе HomePageViewModel, упомянутом выше. Сама страница связана с ней, установив DataContext для подключения к экземпляру статического класса:

DataContext="{Binding BoundHomeModel, Source={StaticResource Locator}}"

Класс HomePageVideModel очень прост:

    public class HomePageViewModel : ViewModelBase
    {
        private ObservableCollection<Download> _downloads;
        public ObservableCollection<Download> Downloads
        {
            get
            {
                return _downloads;
            }
            set
            {
                if (_downloads != value)
                {
                    _downloads = value;
                    RaisePropertyChanged("Downloads");
                }
            }
        }

        public Download CurrentDownload { get; set; }
    }

Здесь я использую ViewModelBase в качестве базового класса, чтобы использовать метод RaisePropertyChanged. Таким образом, проще привязать свойства к элементам, где свойство будет обновлено, и это должно отражаться в состоянии элемента.

Класс ModelLocator также довольно прост:

public class ModelLocator
{
    private static HomePageViewModel _home;

    public ModelLocator()
    {
        HomeStatic.Downloads = new System.Collections.ObjectModel.ObservableCollection<Models.Download>();
    }

    public static HomePageViewModel HomeStatic
        {
            get
            {
                if (_home == null)
                {
                    CreateHome();
                }

                return _home;
            }
        }

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance",
        "CA1822:MarkMembersAsStatic",
        Justification = "This non-static member is needed for data binding purposes.")]
    public HomePageViewModel BoundHomeModel
        {
            get
            {
                return HomeStatic;
            }
        }

    public static void CreateHome()
        {
            if (_home == null)
            {
                _home = new HomePageViewModel();
            }
        }

    public static void Cleanup()
    {

    }
}

Когда ModelLocator впервые инициализируется, я также создаю экземпляр списка загрузок в HomePageViewModel. Эта коллекция фактически является очередью загрузки — она ​​содержит несколько экземпляров модели загрузки. Вместо того, чтобы просто хранить там URL-адреса, я решил, что будет проще сохранить пользовательские экземпляры класса, которые смогут сообщать о его активности и в то же время хранить все необходимые данные. Итак, я придумал этот план для модели Download:

  • Конструктор с экземпляром Uri , который устанавливает местоположение загрузки.
  • DownloadState свойство , которое будет определять , является ли загрузка в очереди или активно.
  • Свойство URL, которое будет получено из вышеупомянутого конструктора.
  • Имя свойство , которое будет определять имя загрузки, так что пользователь легко знает , что загружается для определенного элемента в списке.
  • Прогресс собственность, что в конечном итоге будет переплетено с контролем ProgressBar, показывая ход загрузки.
  • StartDownload метод, который инициирует процесс загрузки для своего собственного экземпляра.

Ради демо, вот весь класс загрузки:

public class Download : ViewModelBase
{
    IsolatedStorageFile file = IsolatedStorageFile.GetUserStoreForApplication();

    public Download(Uri url)
    {
        URL = url;
        TotalLength = -1;
    }

    private DownloadState _state;
    public DownloadState State
    {
        get
        {
            return _state;
        }
        set
        {
            if (_state != value)
            {
                _state = value;
                RaisePropertyChanged("State");
            }
        }
    }

    public Uri URL { get; private set; }
    public string Name { get; set; }

    private int _progress;
    public int Progress
    {
        get
        {
            return _progress;
        }
        set
        {
            if (_progress != value)
            {
                _progress = value;
                RaisePropertyChanged("Progress");
            }
        }
    }

    private long TotalLength { get; set; }

    IsolatedStorageFileStream streamToWriteTo;
    HttpWebRequest request;
    public void StartDownload()
    {
        streamToWriteTo = new IsolatedStorageFileStream(FilenameFormatter.Format(Name) + ".wmv", FileMode.Create, file);
        request = (HttpWebRequest)WebRequest.Create(URL);
        request.AllowReadStreamBuffering = false;
        request.BeginGetResponse(new AsyncCallback(GetData), request);
    }

    
    void GetData(IAsyncResult result)
    {
        HttpWebRequest request = (HttpWebRequest)result.AsyncState;
        HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(result);
        Stream rStream = response.GetResponseStream();

        IsolatedStorageFile file = IsolatedStorageFile.GetUserStoreForApplication();

        byte[] data = new byte[16 * 1024];
        int read;

        long totalValue = response.ContentLength;
        long sum = 0;

        while ((read = rStream.Read(data, 0, data.Length)) > 0)
        {
            sum += read;

            App.DownloadDispatcher.BeginInvoke(new Action(() => Progress = (int)((sum * 100) / totalValue)));

            streamToWriteTo.Write(data, 0, read);
        }
        streamToWriteTo.Close();
        streamToWriteTo.Dispose();

        if (ModelLocator.HomeStatic.Downloads.Count != 0)
        {
            foreach (Download d in ModelLocator.HomeStatic.Downloads)
            {
                if (d.State == DownloadState.Pending)
                {
                    d.State = DownloadState.InProgress;
                    d.StartDownload();
                    break;
                }
            }
        }

        App.DownloadDispatcher.BeginInvoke(new Action(() => ModelLocator.HomeStatic.Downloads.Remove(this)));
    }

}

И вот объяснение каждой части этого. Первое, что я делаю здесь, это создание экземпляра класса IsolatedStorageFile, потому что каждый загружаемый файл напрямую передается в изолированное хранилище без каких-либо промежуточных ссылок (например, MemoryStream).

Перечисление DownloadState содержит только две метки:

public enum DownloadState
{
    InProgress,
    Pending,
} 

Когда начинается загрузка, я создаю запрос HttpWebRequest по URL-адресу, указанному в конструкторе. Вы можете спросить — почему бы не использовать вместо этого WebClient? HttpWebRequest — это более «сырой» класс, и, что удивительно, в некоторых конкретных случаях он работает лучше, чем WebClient, потому что он способен читать непрерывный поток.

При этом я устанавливаю для параметра AllowStreamBuffering значение false, чтобы непрерывно получать загруженные байты вместо получения большого куска двоичных данных, которые необходимо будет сохранить. 

Затем запрос обрабатывается асинхронно. Как только поток получен (не весь поток — только начальная часть), я начинаю процесс постоянного запроса дополнительных данных, пока не загрузю весь файл.

Массив байтов, который я использую для буфера, установлен в 16 КБ. Вы можете настроить это значение в зависимости от размера файла, который вы пытаетесь загрузить. Если это небольшой файл, установите для буфера меньшее значение. Для больших файлов — больший буфер.

Цикл while — это процесс «всегда запрашивать больше данных». В нем я вычисляю прогресс, беря totalValue — длину всего потока, который будет получен, к количеству загружаемых данных (увеличивается каждый раз при каждом чтении буфера).

Я также использую диспетчер для изменения свойства Progress. Я делаю это, потому что свойство привязано к пользовательскому интерфейсу, и его непосредственное изменение может вызвать межпотоковое исключение.

Когда файл загружен, я закрываю поток и проверяю очередь на наличие ожидающих загрузок (которые отмечены состоянием отложенной загрузки). Как только один из них найден, я начинаю загрузку и удаляю текущий экземпляр из очереди.

Вам может быть интересно, почему вся очередь не обрабатывается сразу. Взгляните на класс HomePageViewModel — здесь есть свойство CurrentDownload. Он устанавливается, когда загрузка начинается с любого места в приложении. Таким образом, загрузка не начинается при передаче URL.

Вместо этого, когда загружена страница менеджера загрузок, я использую это:

if (ModelLocator.HomeStatic.CurrentDownload != null)
{
    if (ModelLocator.HomeStatic.Downloads.Count > 0)
    {
        ModelLocator.HomeStatic.CurrentDownload.State = Models.DownloadState.Pending;
        ModelLocator.HomeStatic.Downloads.Add(ModelLocator.HomeStatic.CurrentDownload);
    }
    else
    {
        ModelLocator.HomeStatic.CurrentDownload.State = Models.DownloadState.InProgress;
        ModelLocator.HomeStatic.Downloads.Add(ModelLocator.HomeStatic.CurrentDownload);
        ModelLocator.HomeStatic.CurrentDownload.StartDownload();
    }
}

Если в очереди есть элементы, я устанавливаю текущее состояние загрузки на «Ожидание» и добавляю его в очередь. Если очередь пуста, я добавляю загрузку в очередь и запускаю ее.

Поздравляем! Теперь у вас есть ядро ​​менеджера загрузок для вашего приложения Windows Phone.