×
Namespaces

Variants
Actions

在Windows Phone上选择多张图片

From Nokia Developer Wiki
Jump to: navigation, search
WP Metro Icon Multimedia.png
SignpostIcon WP7 70px.png
Article Metadata

代码示例
兼容于
文章
WS_YiLunLuo 在 13 Jun 2012 创建
最后由 hamishwillee 在 16 Jul 2013 编辑

Contents

简介

Windows Phone SDK提供了一个PhotoChooserTask。使用它你可以写很少的代码,就能让用户选择一张图片。但是,它不支持选择多张图片。如果要让用户选择多张图片,你必须自行提供一个界面。这时候你可以通过XNA的MediaLibrary获取手机上的图片。

本文介绍如何使用XNA的MediaLibrary获取手机上的图片,将它们的缩略图显示在一个自定义界面上,并且让用户选择多张图片。在这个过程中我们也会看到如何使用MVVM设计模式,如何针对256M内存手机优化性能,等等。

选择图片界面:

SelectMultiplePhotos.png

选中的图片们(未全部显示):

MultiplePhotosSelected.png

你可以自File:WPMultiPhotoPicker.zip下载本文附带的示例程序。

View Model

我们使用标准的MVVM设计模式。为此,首先要定义一个view model。

    public class ChoosePhotoViewModel : INotifyPropertyChanged
{
private string _name;
public string Name
{
get { return this._name; }
set
{
if (this._name != value)
{
this._name = value;
this.NotifyPropertyChange("Name");
}
}
}
 
// MediaStream does not need to support notification changed.
public Stream MediaStream { get; set; }
 
private BitmapImage _imageSource;
public BitmapImage ImageSource
{
get
{
if (this.MediaStream == null)
{
return null;
}
 
if (this._imageSource == null)
{
this._imageSource = new BitmapImage();
this._imageSource.SetSource(this.MediaStream);
}
 
return this._imageSource;
}
}
 
public bool IsSelected { get; set; }
 
public event PropertyChangedEventHandler PropertyChanged;
 
protected void NotifyPropertyChange(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this.PropertyChanged, new PropertyChangedEventArgs(propertyName));
}
}
 
/// <summary>
/// We use the name of the photo to do comparison.
/// </summary>
public override bool Equals(object obj)
{
if (obj is ChoosePhotoViewModel)
{
return this.Name.Equals(((ChoosePhotoViewModel)obj).Name);
}
return false;
}
 
public override int GetHashCode()
{
return this.Name.GetHashCode();
}
}

这些代码没什么好多解释的,就是定义一些属性,对于改变时可能需要通知UI的属性实现INotifyPropertyChanged。这些都是标准代码。

需要注意两点:第一,我们重写了Equals方法,这样一来当比较两张图片是否等价时,就不会做一个基于引用的比较,而是做一个基于图片名称的比较了(我们不考虑图片重名的情况)。推荐在重写Equals的情况下也要重写GetHashCode。

第二,ImageSource属性是只读的,我们希望将它绑定到一个Image控件的Source属性,所以需要一个BitmapImage类型的属性。但是平时操作的话,使用Stream就足够了。

界面

我们用一个标准的页面让用户选择图片。在这个页面上创建一个ListBox,用于显示MediaLibrary中的图片。

            <ListBox x:Name="MediaListBox"
ItemTemplate="{StaticResource MediaDataTemplate}"
ItemsPanel="{StaticResource MediaItemsPanelTemplate}"
ItemContainerStyle="{StaticResource MediaListBoxItemStyle}"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Disabled"/>

为了让ListBox按照我们想要的方式排列数据,我们使用Toolkit中提供的WrapPanel定制它的ItemsPanelTemplate,这样一来图片会从上到下排列,如果屏幕上一列排不下,就会在右边出现新的一列。具体控件的使用方法我们就不再复述了。

        <ItemsPanelTemplate x:Key="MediaItemsPanelTemplate">
<toolkit:WrapPanel Orientation="Vertical"/>
</ItemsPanelTemplate>

此外,在ItemTemplate中,我们显示图片,将Image控件的Source属性绑定到view model中。

        <DataTemplate x:Key="MediaDataTemplate">
<Grid>
<Image Source="{Binding ImageSource}" Margin="5"/>
</Grid>
</DataTemplate>

另一方面,默认情况下ListBox的control template不会高亮显示选中的项目,所以我们要自行修改它的template。在template中,我们使用一个CheckBox来表示这个项目是否被选中。

        <Style x:Key="MediaListBoxItemStyle" TargetType="ListBoxItem" BasedOn="{StaticResource PhotoListBoxItemStyle}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border x:Name="LayoutRoot" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" HorizontalAlignment="{TemplateBinding HorizontalAlignment}" VerticalAlignment="{TemplateBinding VerticalAlignment}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="MouseOver"/>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="LayoutRoot">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource TransparentBrush}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="SelectionStates">
<VisualState x:Name="Unselected"/>
<VisualState x:Name="Selected"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<CheckBox Style="{StaticResource MediaCheckBoxStyle}" IsChecked="{Binding IsSelected, Mode=TwoWay}">
<ContentPresenter/>
</CheckBox>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

当然,默认情况下CheckBox选中时会显示一个勾,而我们则希望它用一些亮丽的色彩高亮显示。所以还需要修改CheckBox的template。具体template的代码就不详细解释了,因为很多都是通过Expression Blend生成的。

        <Style x:Key="PhotoListBoxItemStyle" TargetType="ListBoxItem">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="VerticalContentAlignment" Value="Top"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border x:Name="LayoutRoot" HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}" Margin="5" Width="190" Height="190" Background="Transparent" CornerRadius="5">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="MouseOver"/>
<VisualState x:Name="Disabled"/>
</VisualStateGroup>
<VisualStateGroup x:Name="SelectionStates">
<VisualState x:Name="Unselected"/>
<VisualState x:Name="Selected">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="selectionBorder" Storyboard.TargetProperty="(UIElement.Opacity)" To="1" Duration="00:00:00.3"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid>
<Border x:Name="selectionBorder" BorderThickness="3" BorderBrush="#FF427EFF" CornerRadius="5" Opacity="0">
<Border.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFEBF9FF" Offset="0"/>
<GradientStop Color="#FF00B8FF" Offset="0.27"/>
<GradientStop Color="#FF00B3FF" Offset="0.948"/>
<GradientStop Color="#FFCEF1FF" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
</Border>
<ContentPresenter Margin="5" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
 
<Style x:Key="PhoneButtonBase" TargetType="ButtonBase">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource PhoneForegroundBrush}"/>
<Setter Property="Foreground" Value="{StaticResource PhoneForegroundBrush}"/>
<Setter Property="BorderThickness" Value="{StaticResource PhoneBorderThickness}"/>
<Setter Property="FontFamily" Value="{StaticResource PhoneFontFamilySemiBold}"/>
<Setter Property="FontSize" Value="{StaticResource PhoneFontSizeMediumLarge}"/>
<Setter Property="Padding" Value="10,3,10,5"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ButtonBase">
<Grid Background="Transparent">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="MouseOver"/>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="ContentContainer">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneBackgroundBrush}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="ButtonBackground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneForegroundBrush}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="ButtonBackground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneForegroundBrush}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="ContentContainer">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneDisabledBrush}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="ButtonBackground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneDisabledBrush}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="ButtonBackground">
<DiscreteObjectKeyFrame KeyTime="0" Value="Transparent"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border x:Name="ButtonBackground" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" CornerRadius="0" Margin="{StaticResource PhoneTouchTargetOverhang}">
<ContentControl x:Name="ContentContainer" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" Padding="{TemplateBinding Padding}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
 
<Style x:Key="PhoneRadioButtonCheckBoxBase" BasedOn="{StaticResource PhoneButtonBase}" TargetType="ToggleButton">
<Setter Property="Background" Value="{StaticResource PhoneRadioCheckBoxBrush}"/>
<Setter Property="BorderBrush" Value="{StaticResource PhoneRadioCheckBoxBrush}"/>
<Setter Property="FontSize" Value="{StaticResource PhoneFontSizeMedium}"/>
<Setter Property="FontFamily" Value="{StaticResource PhoneFontFamilyNormal}"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Padding" Value="0"/>
</Style>
 
<Style x:Key="MediaCheckBoxStyle" BasedOn="{StaticResource PhoneRadioButtonCheckBoxBase}" TargetType="CheckBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="CheckBox">
<Grid Background="Transparent" Width="190" Height="190" Margin="5">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="MouseOver"/>
<VisualState x:Name="Pressed"/>
<VisualState x:Name="Disabled"/>
</VisualStateGroup>
<VisualStateGroup x:Name="CheckStates">
<VisualState x:Name="Checked">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="selectionBorder" Storyboard.TargetProperty="(UIElement.Opacity)" To="1" Duration="00:00:00.3"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Unchecked"/>
<VisualState x:Name="Indeterminate"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border x:Name="selectionBorder" BorderThickness="3" BorderBrush="#FF427EFF" CornerRadius="5" Opacity="0">
<Border.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFEBF9FF" Offset="0"/>
<GradientStop Color="#FF00B8FF" Offset="0.27"/>
<GradientStop Color="#FF00B3FF" Offset="0.948"/>
<GradientStop Color="#FFCEF1FF" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
</Border>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

此外,在application bar中,我们定义几个按钮,用于操作。

    <phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar IsVisible="True" IsMenuEnabled="True">
<shell:ApplicationBarIconButton
x:Name="PreviousPageButton"
IconUri="/icons/appbar.back.rest.png"
IsEnabled="True"
Text="Previous"
Click="PreviousPageButton_Click"/>
<shell:ApplicationBarIconButton
x:Name="NextPageButton"
IconUri="/icons/appbar.next.rest.png"
IsEnabled="True"
Text="Next"
Click="NextPageButton_Click"/>
<shell:ApplicationBarIconButton
x:Name="OKButton"
IconUri="/icons/appbar.check.rest.png"
IsEnabled="True"
Text="OK"
Click="OKButton_Click"/>
<shell:ApplicationBarIconButton
x:Name="CancelButton"
IconUri="/icons/appbar.cancel.rest.png"
IsEnabled="True"
Text="Cancel"
Click="CancelButton_Click"/>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>

你可以自Program Files (x86)\Microsoft SDKs\Windows Phone\v7.1\Icons目录下找到很多Windows Phone SDK自带的图标,你有权利在自己的程序中使用它们。

代码逻辑

接下来我们来实现代码逻辑。

首先定义一些变量:

        private ObservableCollection<ChoosePhotoViewModel> _photoDataSource;
private List<ChoosePhotoViewModel> _selectedPhotos;
private int _currentPage = 0;
private int _pageSize = 9;

这里有两个变量用于分页。因为用户的媒体库中可能有很多很多图片,就算使用缩略图,一次性将它们全部加载近内存还是可能会导致内存使用超过60M(通常我们要求大家的程序内存使用量低于60M,否则在256M内存的手机,例如Lumia 610上,可能会有很严重的性能问题)。我们这里每页显示9幅画,这样刚好够在一屏幕上显示大家可以根据情况修改这个数值。

此外,_photoDataSource用于保存当前页面的数据源(它的数量不会超过9个),而_selectedPhotos则用于保存用户选中的图片(为了在跨页面选择时保存之前页面的数据)。

还有,为了方便,在App类中,我们创建一个属性保存选中的图片,这个属性可以被其它页面访问。

        public static ObservableCollection<ChoosePhotoViewModel> PhotoCollection = new ObservableCollection<ChoosePhotoViewModel>();

在OnNavigatedTo方法中,我们初始化这些变量,并且获取第一页的图片。

        protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
this._photoDataSource = new ObservableCollection<ChoosePhotoViewModel>();
this._selectedPhotos = new List<ChoosePhotoViewModel>();
this.GetPicturesForCurrentPage();
this.MediaListBox.ItemsSource = _photoDataSource;
 
base.OnNavigatedTo(e);
}

以下是GetPicturesForCurrentPage的实现,它对所有页是通用的,而不仅仅针对第一页有效:

        /// <summary>
/// It's not a good idea to display all photos at once.
/// This method implements paging.
/// </summary>
private void GetPicturesForCurrentPage()
{
// Page number should not be less than 0.
if (this._currentPage < 0)
{
this._currentPage = 0;
return;
}
 
MediaLibrary mediaLibrary = new MediaLibrary();
 
// Already the last page.
int pageCount = mediaLibrary.Pictures.Count / this._pageSize;
if (this._currentPage > pageCount)
{
this._currentPage = pageCount;
return;
}
 
// Store the selection.
this.StoreSelection();
 
// Reset the data source.
this._photoDataSource.Clear();
 
var picturesOnCurrentPage = mediaLibrary.Pictures.Skip(this._currentPage * this._pageSize).Take(this._pageSize);
foreach (var picture in picturesOnCurrentPage)
{
Stream pictureStream = picture.GetThumbnail();
ChoosePhotoViewModel viewModel = new ChoosePhotoViewModel()
{
Name = picture.Name,
MediaStream = pictureStream
};
 
// In view model, we override Equals to compare the name of the photo rather than the reference.
// So if a photo with the same name is found in selected photos, Contains will return true.
if (this._selectedPhotos.Contains(viewModel))
{
viewModel.IsSelected = true;
}
 
_photoDataSource.Add(viewModel);
}
}

在这里我们先调用StoreSelection(代码会在后面给出)保存前一页上选中的数据,从而保证今后返回到这一页时选择不会丢失。

然后我们使用XNA的MediaLibrary获取当前页的图片。MediaLibrary.Pictures属性实现了IEnumerable,所以它支持标准的LINQ的Skip和Take方法,我们的分页逻辑因此可以大幅简化。如果你不熟悉标准的LINQ分页,可以参考http://msdn.microsoft.com/en-us/library/bb386988.aspx。虽然这篇文档针对的是LINQ to SQL ,但是对于一般的LINQ也是适用的。

在获取了图片之后,我们不急于将它打开,而是通过GetThumbnail方法获取一个缩略图。在我们的图片选择界面上我们根本不需要显示完整大小的图片,缩略图就足够了。使用缩略图可以大大降低内存使用。

接下来就是针对每一幅画创建一个view model了。注意这个方法是针对所有页通用的,所以我们要考虑用户可能从第二页返回第一页,因此需要判断这幅画是不是曾经被选中过。

下面再给出StoreSelection方法:

        /// <summary>
/// Add selected photos on the current page to selected photo list,
/// and close unselected photos.
/// </summary>
private void StoreSelection()
{
foreach (ChoosePhotoViewModel photo in this._photoDataSource)
{
if (photo.IsSelected)
{
if (!this._selectedPhotos.Contains(photo))
{
this._selectedPhotos.Add(photo);
}
}
// If the media is not chosen, close the thumbnail stream.
else
{
if (this._selectedPhotos.Contains(photo))
{
this._selectedPhotos.Remove(photo);
}
photo.MediaStream.Close();
}
}
}

这个方法在用户切换页时调用,用于存储当前页上选中的图片。没什么好多解释的,唯一需要注意的是如果一幅画没有被选中,我们就直接把它的thumbnail close掉,从而释放内存。

现在,在翻页按钮被点下时,我们只需要修改_currentPage,并且调用GetPicturesForCurrentPage就可以了。

        private void PreviousPageButton_Click(object sender, System.EventArgs e)
{
this._currentPage--;
this.GetPicturesForCurrentPage();
}
 
private void NextPageButton_Click(object sender, System.EventArgs e)
{
this._currentPage++;
this.GetPicturesForCurrentPage();
}

最后,当用户点击OK, Cancel按钮时,我们是情况返回选中的图片,或者扔掉所有的选中的项目:

        private void OKButton_Click(object sender, System.EventArgs e)
{
this.StoreSelection();
 
// Add all selected photos to App.MediaCollection.
foreach (ChoosePhotoViewModel photo in this._selectedPhotos)
{
// Return the original photo stream rather than the thumbnail.
photo.MediaStream = this.GetOriginalImage(photo.Name);
App.PhotoCollection.Add(photo);
}
 
// Go back to the calling page.
this.NavigationService.GoBack();
}
 
private void CancelButton_Click(object sender, System.EventArgs e)
{
// Close all thumbnail streams.
foreach (ChoosePhotoViewModel photo in this._photoDataSource)
{
photo.MediaStream.Close();
}
 
// Go back to the calling page without doing anything.
this.NavigationService.GoBack();
}

我们之前一直在用缩略图,但是返回图片时最好返回原是图片的stream,而不是缩略图。因此我们写了一个GetOriginalImage方法:

        /// <summary>
/// Get the original image from XNA media library.
/// </summary>
/// <param name="name">The name of the image.</param>
/// <returns>A stream representing the resized image, in jpeg format.</returns>
public Stream GetOriginalImage(string name)
{
MediaLibrary mediaLibrary = new MediaLibrary();
PictureCollection pictureCollection = mediaLibrary.Pictures;
Picture picture = pictureCollection.Where(p => p.Name == name).FirstOrDefault();
if (picture == null)
{
throw new InvalidOperationException(string.Format("Cannot load the picture {0}. Possibly the picture has been deleted.", name));
}
Stream originalImageStream = picture.GetImage();
originalImageStream.Position = 0;
return originalImageStream;
}

在这个方法中,我们使用LINQ根据图片的名字从media library中找到对应的图片,并且打开它。

测试

在MainPage中让我们做一个简单的测试。使用一个ListBox来显示选中的图片:

            <ListBox
x:Name="photoListBox"
ItemTemplate="{StaticResource PhotoDataTemplate}"
Grid.Row="1"/>

这里的ItemTemplate中,我们不指定图片大小,因此会撑满屏幕。如果你发现图片清晰度好象很低,就说明你可能之前没有返回原是图片流,而是返回了缩略图。

        <DataTemplate x:Key="PhotoDataTemplate">
<Grid>
<Image Source="{Binding ImageSource}" Margin="5"/>
</Grid>
</DataTemplate>

在代码中间,我们简单判断是否有选中的图片,如果没有,就跳转到选择图片的页面。

        protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
if (App.PhotoCollection.Count == 0)
{
this.NavigationService.Navigate(new Uri("/ChoosePhotoPage.xaml", UriKind.Relative));
}
else
{
this.photoListBox.ItemsSource = App.PhotoCollection;
}
base.OnNavigatedTo(e);
}

总结

因为Windows Phone自己没有提供选择多张图片的功能,我们不得不自己实现。可以看到,要做这样一个功能,还是需要花不少功夫的。所以我们要感谢系统提供的那么多组件才行啊,它们帮助我们省下了很多很多心力,让我们可以集中思考组装自己的程序,而不是如何创建每一个组件。

This page was last modified on 16 July 2013, at 07:23.
147 page views in the last 30 days.

Was this page helpful?

Your feedback about this content is important. Let us know what you think.

 

Thank you!

We appreciate your feedback.

×