Estudo de caso do Windows Phone Silverlight para UWP: Bookstore2
Este estudo de caso, que se baseia nas informações fornecidas em Bookstore1, começa com um aplicativo do Windows Phone Silverlight que exibe dados agrupados em um LongListSelector. No modelo de exibição, cada instância da classe Author representa o grupo de livros escritos por esse autor e, no LongListSelector, podemos exibir a lista de livros agrupados por autor ou podemos diminuir o zoom para ver uma lista de atalhos de autores. A lista de atalhos proporciona uma navegação mais rápida do que rolar pela lista de livros. Percorremos as etapas de portabilidade do aplicativo para um aplicativo UWP (Plataforma Universal do Windows) do Windows 10.
Observação Ao abrir Bookstore2Universal_10 no Visual Studio, se você vir a mensagem "Atualização do Visual Studio necessária", siga as etapas para definir a Versão da Plataforma de Destino em TargetPlatformVersion.
Downloads
Baixe o aplicativo Bookstore2WPSL8 Windows Phone Silverlight.
Baixe o aplicativo Bookstore2Universal_10 Windows 10.
O aplicativo Windows Phone Silverlight
A ilustração abaixo mostra a aparência do Bookstore2WPSL8, o aplicativo que vamos portar. É um LongListSelector de rolagem vertical de livros agrupados por autor. Você pode diminuir o zoom para a lista de atalhos e, a partir daí, navegar de volta para qualquer grupo. Há duas partes principais desse aplicativo: o modelo de exibição que fornece a fonte de dados agrupada e a interface do usuário que se associa a esse modelo de exibição. Como veremos, essas duas peças são facilmente transferidas da tecnologia Windows Phone Silverlight para a Plataforma Universal do Windows (UWP).
Portabilidade para um projeto do Windows 10
É uma tarefa rápida criar um novo projeto no Visual Studio, copiar arquivos para ele de Bookstore2WPSL8 e incluir os arquivos copiados no novo projeto. Comece criando um novo projeto de Aplicativo em Branco (Windows Universal). Nomeie-o Bookstore2Universal_10. Estes são os arquivos a serem copiados de Bookstore2WPSL8 para Bookstore2Universal_10.
- Copie a pasta que contém os arquivos PNG da imagem da capa do livro (a pasta é \Assets\CoverImages). Depois de copiar a pasta, no Gerenciador de Soluções, verifique se Mostrar Todos os Arquivos está ativado. Clique com o botão direito do mouse na pasta que você copiou e clique em Incluir no projeto. Esse comando é o que queremos dizer com "incluir" arquivos ou pastas em um projeto. Sempre que você copiar um arquivo ou pasta, clique em Atualizar no Gerenciador de Soluções e inclua o arquivo ou pasta no projeto. Não há necessidade de fazer isso para arquivos que você está substituindo no destino.
- Copie a pasta que contém o arquivo de origem do modelo de exibição (a pasta é \ViewModel).
- Copie MainPage.xaml e substitua o arquivo no destino.
Podemos manter o App.xaml e App.xaml.cs que o Visual Studio gerou para nós no projeto do Windows 10.
Edite o código-fonte e os arquivos de marcação que você acabou de copiar e altere todas as referências ao namespace Bookstore2WPSL8 para Bookstore2Universal_10. Uma maneira rápida de fazer isso é usar o recurso Substituir em arquivos . No código imperativo no arquivo de origem do modelo de exibição, essas alterações de portabilidade são necessárias.
- Altere
System.ComponentModel.DesignerProperties
paraDesignMode
e use o comando Resolver nele. Exclua a propriedade e use oIsInDesignTool
IntelliSense para adicionar o nome correto da propriedade:DesignModeEnabled
. - Use o comando Resolver em
ImageSource
. - Use o comando Resolver em
BitmapImage
. - Exclua
using System.Windows.Media;
eusing System.Windows.Media.Imaging;
. - Altere o valor retornado pela propriedade Bookstore2Universal_10.BookstoreViewModel.AppName de "BOOKSTORE2WPSL8" para "BOOKSTORE2UNIVERSAL".
- Assim como fizemos para Bookstore1, atualize a implementação da propriedade BookSku.CoverImage (consulte Associando uma imagem a um modelo de exibição).
Em MainPage.xaml, essas alterações iniciais de portabilidade são necessárias.
- Altere
phone:PhoneApplicationPage
paraPage
(incluindo as ocorrências na sintaxe do elemento de propriedade). - Exclua as declarações de prefixo de
phone
namespace eshell
. - Altere "clr-namespace" para "using" na declaração de prefixo de namespace restante.
- Exclua
SupportedOrientations="Portrait"
eOrientation="Portrait"
configure Retrato no manifesto do pacote do aplicativo no novo projeto. - Excluir
shell:SystemTray.IsVisible="True"
. - Os tipos de conversores de item da lista de atalhos (que estão presentes na marcação como recursos) foram movidos para o namespace Windows.UI.Xaml.Controls.Primitives. Portanto, adicione a declaração de prefixo de namespace Windows_UI_Xaml_Controls_Primitives e mapeie-a para Windows.UI.Xaml.Controls.Primitives. Nos recursos do conversor de itens da lista de atalhos, altere o prefixo de
phone:
paraWindows_UI_Xaml_Controls_Primitives:
. - Assim como fizemos para Bookstore1, substitua todas as referências ao
PhoneTextExtraLargeStyle
estilo TextBlock por uma referência aSubtitleTextBlockStyle
, substituaPhoneTextSubtleStyle
porSubtitleTextBlockStyle
, substituaPhoneTextNormalStyle
porCaptionTextBlockStyle
, e substituaPhoneTextTitle1Style
porHeaderTextBlockStyle
. - Há uma exceção no
BookTemplate
. O estilo do segundo TextBlock deve fazer referência aCaptionTextBlockStyle
. - Remova o atributo FontFamily do TextBlock interno
AuthorGroupHeaderTemplate
e defina o Background of the Border como referênciaSystemControlBackgroundAccentBrush
em vez dePhoneAccentBrush
. - Devido às alterações relacionadas aos pixels de exibição, percorra a marcação e multiplique qualquer dimensão de tamanho fixo (margens, largura, altura, etc.) por 0,8.
Substituindo o LongListSelector
Substituir o LongListSelector por um controle SemanticZoom levará várias etapas, portanto, vamos começar com isso. Um LongListSelector é associado diretamente à fonte de dados agrupada, mas um SemanticZoom contém controles ListView ou GridView, que se associam indiretamente aos dados por meio de um adaptador CollectionViewSource. O CollectionViewSource precisa estar presente na marcação como um recurso, portanto, vamos começar adicionando-o à marcação em MainPage.xaml dentro <Page.Resources>
do .
<CollectionViewSource
x:Name="AuthorHasACollectionOfBookSku"
Source="{Binding Authors}"
IsSourceGrouped="true"/>
Observe que a associação em LongListSelector.ItemsSource se torna o valor de CollectionViewSource.Source e LongListSelector.IsGroupingEnabled se torna CollectionViewSource.IsSourceGrouped. O CollectionViewSource tem um nome (observação: não uma chave, como você pode esperar) para que possamos associá-lo.
Em seguida, substitua o phone:LongListSelector
por essa marcação, o que nos dará um SemanticZoom preliminar para trabalhar.
<SemanticZoom>
<SemanticZoom.ZoomedInView>
<ListView
ItemsSource="{Binding Source={StaticResource AuthorHasACollectionOfBookSku}}"
ItemTemplate="{StaticResource BookTemplate}">
<ListView.GroupStyle>
<GroupStyle
HeaderTemplate="{StaticResource AuthorGroupHeaderTemplate}"
HidesIfEmpty="True"/>
</ListView.GroupStyle>
</ListView>
</SemanticZoom.ZoomedInView>
<SemanticZoom.ZoomedOutView>
<ListView
ItemsSource="{Binding CollectionGroups, Source={StaticResource AuthorHasACollectionOfBookSku}}"
ItemTemplate="{StaticResource ZoomedOutAuthorTemplate}"/>
</SemanticZoom.ZoomedOutView>
</SemanticZoom>
A noção LongListSelector dos modos de lista simples e lista de atalhos é respondida na noção SemanticZoom de uma exibição ampliada e reduzida, respectivamente. O modo de exibição ampliado é uma propriedade e você define essa propriedade como uma instância de um ListView. Nesse caso, o modo de exibição reduzido também é definido como um ListView e ambos os controles ListView estão associados ao nosso CollectionViewSource. O modo de exibição ampliado usa o mesmo modelo de item, modelo de cabeçalho de grupo e configuração HideEmptyGroups (agora chamado HidesIfEmpty) que a lista simples do LongListSelector. E a visualização reduzida usa um modelo de item muito parecido com o que está dentro do estilo de lista de atalhos do LongListSelector (AuthorNameJumpListStyle
). Além disso, observe que a exibição reduzida é associada a uma propriedade especial do CollectionViewSource chamada CollectionGroups, que é uma coleção que contém os grupos em vez dos itens.
Não precisamos AuthorNameJumpListStyle
mais, pelo menos não de tudo. Precisamos apenas do modelo de dados para os grupos (que são autores neste aplicativo) na visualização reduzida. Então, excluímos o AuthorNameJumpListStyle
estilo e o substituímos por esse modelo de dados.
<DataTemplate x:Key="ZoomedOutAuthorTemplate">
<Border Margin="9.6,0.8" Background="{Binding Converter={StaticResource JumpListItemBackgroundConverter}}">
<TextBlock Margin="9.6,0,9.6,4.8" Text="{Binding Group.Name}" Style="{StaticResource SubtitleTextBlockStyle}"
Foreground="{Binding Converter={StaticResource JumpListItemForegroundConverter}}" VerticalAlignment="Bottom"/>
</Border>
</DataTemplate>
Observe que, como o contexto de dados desse modelo de dados é um grupo em vez de um item, associamos a uma propriedade especial chamada Grupo.
Você pode criar e executar o aplicativo agora. Veja como fica no emulador móvel.
O modelo de exibição e as exibições ampliadas e reduzidas estão funcionando juntas corretamente, embora um problema seja que precisamos fazer um pouco mais de trabalho de estilo e modelagem. Por exemplo, os estilos e pincéis corretos ainda não estão sendo usados, portanto, o texto fica invisível nos cabeçalhos do grupo nos quais você pode clicar para diminuir o zoom. Se você executar o aplicativo em um dispositivo desktop, verá um segundo problema, que é que o aplicativo ainda não adapta sua interface do usuário para oferecer a melhor experiência e uso do espaço em dispositivos maiores, onde as janelas podem ser potencialmente muito maiores do que a tela de um dispositivo móvel. Portanto, nas próximas seções (Estilo inicial e modelagem, Interface adaptável e Estilo final), corrigiremos esses problemas.
Estilo inicial e modelos
Para espaçar bem os cabeçalhos dos grupos, edite AuthorGroupHeaderTemplate
e defina uma margem de "0,0,0,9.6"
na borda.
Para espaçar bem os itens do livro, edite BookTemplate
e defina a margem como "9.6,0"
em ambos os TextBlocks.
Para definir um pouco melhor o nome do aplicativo e o título da página, dentro TitlePanel
de , remova a margem superior no segundo TextBlock definindo o valor como "7.2,0,0,0"
. E por TitlePanel
si só, defina a margem para 0
(ou qualquer valor que pareça bom para você)
Altere LayoutRoot
o plano de fundo de "{ThemeResource ApplicationPageBackgroundThemeBrush}"
.
Interface adaptável
Como começamos com um aplicativo de telefone, não é surpresa que o layout da interface do usuário do nosso aplicativo portado realmente só faça sentido para dispositivos pequenos e janelas estreitas neste estágio do processo. Mas, gostaríamos muito que o layout da interface do usuário se adaptasse e fizesse melhor uso do espaço quando o aplicativo está sendo executado em uma janela ampla (o que só é possível em um dispositivo com uma tela grande) e que ele use apenas a interface do usuário que temos atualmente quando a janela do aplicativo é estreita (o que acontece em um dispositivo pequeno, e também pode acontecer em um dispositivo grande).
Podemos usar o recurso adaptável do Visual State Manager para conseguir isso. Definiremos propriedades em elementos visuais para que, por padrão, a interface do usuário seja disposta no estado estreito usando os modelos que estamos usando agora. Em seguida, detectaremos quando a janela do aplicativo é mais larga ou igual a um tamanho específico (medido em unidades de pixels efetivos) e, em resposta, alteraremos as propriedades dos elementos visuais para obter um layout maior e mais amplo. Colocaremos essas alterações de propriedade em um estado visual e usaremos um gatilho adaptável para monitorar continuamente e determinar se devemos aplicar esse estado visual ou não, dependendo da largura da janela em pixels efetivos. Estamos disparando na largura da janela neste caso, mas também é possível acionar na altura da janela.
Uma largura mínima de janela de 548 epx é apropriada para este caso de uso porque esse é o tamanho do menor dispositivo em que gostaríamos de mostrar o layout amplo. Os telefones são normalmente menores que 548 epx, portanto, em um dispositivo pequeno como esse, permaneceríamos no layout estreito padrão. Em um PC, a janela será iniciada por padrão larga o suficiente para disparar a mudança para o estado wide, que exibirá itens de tamanho 250x250. A partir daí, você poderá arrastar a janela estreita o suficiente para exibir um mínimo de duas colunas dos itens 250x250. Mais estreito do que isso e o gatilho será desativado, o estado visual amplo será removido e o layout estreito padrão estará em vigor.
Antes de abordar a parte adaptativa do Visual State Manager, primeiro precisamos projetar o estado amplo e isso significa adicionar alguns novos elementos visuais e modelos à nossa marcação. Estas etapas descrevem como fazer isso. Por meio de convenções de nomenclatura para elementos visuais e modelos, incluiremos a palavra "wide" no nome de qualquer elemento ou modelo que seja para o estado wide. Se um elemento ou modelo não contiver a palavra "wide", você poderá supor que ele é para o estado estreito, que é o estado padrão e cujos valores de propriedade são definidos como valores locais em elementos visuais na página. Somente os valores de propriedade para o estado largo são definidos por meio de um Estado Visual real na marcação.
- Faça uma cópia do controle SemanticZoom na marcação e defina
x:Name="narrowSeZo"
a cópia. No original, definax:Name="wideSeZo"
e também definaVisibility="Collapsed"
para que o amplo não seja visível por padrão. - Em
wideSeZo
, altere o ListViews para GridViews no modo de exibição ampliado e no modo de exibição reduzido. - Faça uma cópia desses três recursos
AuthorGroupHeaderTemplate
,ZoomedOutAuthorTemplate
eBookTemplate
anexe a palavraWide
às chaves das cópias. Além disso, atualizewideSeZo
para que ele faça referência às chaves desses novos recursos. - Substitua o conteúdo de
AuthorGroupHeaderTemplateWide
por<TextBlock Style="{StaticResource SubheaderTextBlockStyle}" Text="{Binding Name}"/>
. - Substitua o conteúdo de
ZoomedOutAuthorTemplateWide
por:
<Grid HorizontalAlignment="Left" Width="250" Height="250" >
<Border Background="{StaticResource ListViewItemPlaceholderBackgroundThemeBrush}"/>
<StackPanel VerticalAlignment="Bottom" Background="{StaticResource ListViewItemOverlayBackgroundThemeBrush}">
<TextBlock Foreground="{StaticResource ListViewItemOverlayForegroundThemeBrush}"
Style="{StaticResource SubtitleTextBlockStyle}"
Height="80" Margin="15,0" Text="{Binding Group.Name}"/>
</StackPanel>
</Grid>
- Substitua o conteúdo de
BookTemplateWide
por:
<Grid HorizontalAlignment="Left" Width="250" Height="250">
<Border Background="{StaticResource ListViewItemPlaceholderBackgroundThemeBrush}"/>
<Image Source="{Binding CoverImage}" Stretch="UniformToFill"/>
<StackPanel VerticalAlignment="Bottom" Background="{StaticResource ListViewItemOverlayBackgroundThemeBrush}">
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}"
Foreground="{StaticResource ListViewItemOverlaySecondaryForegroundThemeBrush}"
TextWrapping="NoWrap" TextTrimming="CharacterEllipsis"
Margin="12,0,24,0" Text="{Binding Title}"/>
<TextBlock Style="{StaticResource CaptionTextBlockStyle}" Text="{Binding Author.Name}"
Foreground="{StaticResource ListViewItemOverlaySecondaryForegroundThemeBrush}" TextWrapping="NoWrap"
TextTrimming="CharacterEllipsis" Margin="12,0,12,12"/>
</StackPanel>
</Grid>
- Para o estado amplo, os grupos na visualização ampliada precisarão de mais espaço vertical para respirar ao seu redor. Criar e referenciar um modelo de painel de itens nos dará os resultados desejados. Veja como fica a marcação.
<ItemsPanelTemplate x:Key="ZoomedInItemsPanelTemplate">
<ItemsWrapGrid Orientation="Horizontal" GroupPadding="0,0,0,20"/>
</ItemsPanelTemplate>
...
<SemanticZoom x:Name="wideSeZo" ... >
<SemanticZoom.ZoomedInView>
<GridView
...
ItemsPanel="{StaticResource ZoomedInItemsPanelTemplate}">
...
- Por fim, adicione a marcação apropriada do Visual State Manager como o primeiro filho do
LayoutRoot
.
<Grid x:Name="LayoutRoot" ... >
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState x:Name="WideState">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="548"/>
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="wideSeZo.Visibility" Value="Visible"/>
<Setter Target="narrowSeZo.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
...
Estilo final
Tudo o que resta são alguns ajustes finais de estilo.
- Em
AuthorGroupHeaderTemplate
, definaForeground="White"
o TextBlock para que ele pareça correto ao ser executado na família de dispositivos móveis. - Adicione ao TextBlock em ambos e
ZoomedOutAuthorTemplate
AuthorGroupHeaderTemplate
.FontWeight="SemiBold"
- No
narrowSeZo
, os cabeçalhos do grupo e os autores na visualização reduzida são alinhados à esquerda em vez de esticados, então vamos trabalhar nisso. Criaremos um HeaderContainerStyle para o modo de exibição ampliado com HorizontalContentAlignment definido comoStretch
. E criaremos um ItemContainerStyle para a exibição reduzida contendo o mesmo Setter. Aqui está o que parece.
<Style x:Key="AuthorGroupHeaderContainerStyle" TargetType="ListViewHeaderItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
<Style x:Key="ZoomedOutAuthorItemContainerStyle" TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
...
<SemanticZoom x:Name="narrowSeZo" ... >
<SemanticZoom.ZoomedInView>
<ListView
...
<ListView.GroupStyle>
<GroupStyle
...
HeaderContainerStyle="{StaticResource AuthorGroupHeaderContainerStyle}"
...
<SemanticZoom.ZoomedOutView>
<ListView
...
ItemContainerStyle="{StaticResource ZoomedOutAuthorItemContainerStyle}"
...
Essa última sequência de operações de estilo deixa o aplicativo com esta aparência.
O aplicativo Windows 10 portado em execução em um dispositivo Desktop, exibição ampliada, dois tamanhos de janela
O aplicativo Windows 10 portado em execução em um dispositivo da área de trabalho, exibição reduzida, dois tamanhos de janela
O aplicativo Windows 10 portado em execução em um dispositivo móvel, exibição ampliada
O aplicativo Windows 10 portado em execução em um dispositivo móvel, exibição reduzida
Tornando o modelo de exibição mais flexível
Esta seção contém um exemplo de recursos que se abrem para nós em virtude de termos movido nosso aplicativo para usar a UWP. Aqui, explicamos as etapas opcionais que você pode seguir para tornar seu modelo de exibição mais flexível quando acessado por meio de um CollectionViewSource. O modelo de exibição (o arquivo de origem está em ViewModel\BookstoreViewModel.cs) que portamos do aplicativo Bookstore2WPSL8 do Windows Phone Silverlight contém uma classe chamada Author, que deriva da Lista<T>, em que T é BookSku. Isso significa que a classe Author é um grupo de BookSku.
Quando associamos CollectionViewSource.Source a Authors, a única coisa que estamos comunicando é que cada Author em Authors é um grupo de algo. Deixamos para o CollectionViewSource determinar que o Autor é, neste caso, um grupo de BookSku. Isso funciona: mas não é flexível. E se quisermos que o autor seja um grupo de BookSku e um grupo dos endereços onde o autor viveu? O autor não pode ser os dois grupos. Mas, o autor pode ter qualquer número de grupos. E essa é a solução: use o padrão tem um grupo em vez de, ou além do padrão é-um-grupo que estamos usando atualmente. Este é o procedimento:
- Altere o autor para que ele não derive mais da Lista<T>.
- Adicione este campo a
- Adicione esta propriedade a
- E, claro, podemos repetir as duas etapas acima para adicionar quantos grupos precisarmos ao Autor.
- Altere a implementação do método AddBookSku para
this.BookSkus.Add(bookSku);
. - Agora que o Autor tem pelo menos um grupo, precisamos comunicar ao CollectionViewSource qual desses grupos ele deve usar. Para fazer isso, adicione esta propriedade ao CollectionViewSource:
ItemsPath="BookSkus"
Essas alterações deixam esse aplicativo funcionalmente inalterado, mas agora você sabe como estender Author e CollectionViewSource, caso seja necessário. Vamos fazer uma última alteração em Author para que, se o usarmos sem especificar CollectionViewSource.ItemsPath, um grupo padrão de nossa escolha seja usado:
public class Author : IEnumerable<BookSku>
{
...
public IEnumerator<BookSku> GetEnumerator()
{
return this.BookSkus.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.BookSkus.GetEnumerator();
}
}
E agora podemos optar por remover ItemsPath="BookSkus"
se quisermos e o aplicativo ainda se comportará da mesma maneira.
Conclusão
Este estudo de caso envolveu uma interface de usuário mais ambiciosa do que a anterior. Descobriu-se que todos os recursos e conceitos do LongListSelector do Windows Phone Silverlight — e muito mais — estavam disponíveis para um aplicativo UWP na forma de SemanticZoom, ListView, GridView e CollectionViewSource. Mostramos como reutilizar, copiar e editar, o código imperativo e a marcação em um aplicativo UWP para obter funcionalidade, interface do usuário e interações personalizadas para atender aos fatores forma de dispositivo Windows mais estreitos e amplos e a todos os tamanhos intermediários.