Plataforma UNO - Criando um App ToDo - parte 3
Plataforma UNO - Plataforma UNO - Part 3
Na última parte da série, lançamos algum trabalho de base. Conseguimos ter a construção básica disponível, mas ainda não havia interação possível com o usuário. Nesta parte da série vamos mudar isso. Para saber para onde estamos indo, vamos dar uma espiada nisso:
Criando o componente de diálogo
Primeiro precisamos adicionar um novo ContentDialog ao nosso projeto. Assim como nos elementos UserControl, você pode clicar com o botão direito do mouse em seu projeto compartilhado e pressionar Adicionar. Lá você encontra o modelo ContentDialog oferecido pela Plataforma Uno.
Uma vez criado, mude para o arquivo recém-criado. Você verá algo assim:
<ContentDialog
x:Class="TodoApp.ContentDialog1"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:TodoApp"
xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="TITLE"
PrimaryButtonText="Button1"
SecondaryButtonText="Button2"
PrimaryButtonClick="ContentDialog_PrimaryButtonClick"
SecondaryButtonClick="ContentDialog_SecondaryButtonClick">
<Grid>
</Grid>
</ContentDialog>
O ContentDialog traz muita utilidade com ele. Você pode ver que alteramos, por exemplo, o título ou PrimaryButtonText, então vamos fazer isso por enquanto. Eu também removo os dois manipuladores de eventos Click. Voltaremos a adicioná-los mais tarde com alguma lógica mais útil. Seu xaml deve ser semelhante a este:
<ContentDialog
x:Class="TodoApp.AddTodoItemDialog"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:TodoApp"
xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
Title="Add new todo item"
PrimaryButtonText="Add"
IsPrimaryButtonEnabled="False"
SecondaryButtonText="Cancel"
Agora que temos o esqueleto do nosso ContentDialog, podemos exibi-lo. Você se lembra na última parte em que criamos o botão estiloso? Vamos aproveitar este botão para fazer algum código real agora. Vá para AddTodoItem.xaml e adicione um evento Click ao botão como este:_ <Button Width="64" Height="64" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"_ Click="OpenDialog">. A única parte que falta agora é que temos que declarar um método no arquivo code-behind AddTodoItem.xaml.cs para abrir nossa mágica recém-criada. Para isso vamos criar o método OpenDialog e fazer o seguinte:
private void OpenDialog(object sender, RoutedEventArgs args)
{
var dialog = new AddTodoItemDialog();
dialog.ShowAsync();
}
Nós apenas criamos uma nova instância de nosso AddTodoItemDialog e chamamos ShowAsync. Agora você pode compilar seu código e experimentar seu botão:
Agora que não parece ruim, não é? No meu exemplo, o botão Adicionar está desabilitado porque está definido IsPrimaryButtonEnabled="False" no ContentDialog. Agora o próximo passo é preencher nosso diálogo com um pouco de vida. Vamos adicionar todos os controles necessários para criar um novo item de tarefas. Depois cuidaremos do nosso View-Model incluindo algumas validações. Agora eu quero três coisas do usuário:
- O título, que é obrigatório
- Uma data de vencimento, que é opcional
- Uma descrição, que pode ser mais longa (suporte a várias linhas) e também é obrigatória Para iniciantes, usaremos um layout de Grade simples para alinhar todos os itens:
<ContentDialog
x:Class="TodoApp.ContentDialog1"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:TodoApp"
xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="using:MADE.UI.Controls"
Title="Add new todo item"
PrimaryButtonText="Add"
IsPrimaryButtonEnabled="False"
SecondaryButtonText="Cancel">
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="10" />
<RowDefinition Height="Auto" />
<RowDefinition Height="10" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Title:" />
<TextBox Grid.Row="0" Grid.Column="2"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Due Date:" />
<DatePicker Grid.Row="2" Grid.Column="2"/>
<TextBlock Grid.Row="4" Grid.Column="0" Text="Description:" />
<TextBox Grid.Row="4" Grid.Column="2" AcceptsReturn="True" TextWrapping="Wrap" />
</Grid>
</ContentDialog>
Temos 3 colunas e 5 linhas. Eu uso a primeira coluna para nossos rótulos e a última coluna para os controles de entrada. A segunda coluna é apenas para ter algum espaço entre... razão puramente óptica. É claro que você também pode trabalhar com preenchimento ou margem, o que preferir. O mesmo eu faço com as linhas. Cada segunda linha é apenas para ter algum espaço entre elas, para que pareça mais atraente. Apenas a última linha é importante. Nosso último TextBox é destinado à descrição, que permite multilinhas (AcceptsReturn="True"), portanto, pode aumentar de tamanho. A grade deve levar isso em consideração, portanto, o "*" como Altura para essa linha específica. Se compilarmos o resultado e executarmos nosso exemplo, você verá algo assim:
Isso parece muito bom, não é? Claro que se você quiser pode estender isso para todos os atributos que você gostaria de pedir ao usuário. Eu queria ter o mínimo. A próxima etapa é criar um modelo de exibição que posteriormente retenha os dados e possa ser utilizado para validação. Antes de fazermos isso, vamos importar dois pacotes nuget. Estamos usando-os para simplificar nossa validação e adicionar INotifyPropertyChanged pronto para uso.
- Refractored.MvvmHelpers para o padrão observável. No site oficial da Plataforma Uno você pode saber mais
- MADE.UI.Controls.Validator para validação.
Como dito na parte 1, o próprio projeto compartilhado é apenas um contêiner, portanto, você não pode adicionar referências a ele. Em vez disso, você adicionará todo o pacote nuget necessário a todos os projetos do Head.
Ver modelo e validação
Basicamente, peguei o tutorial de James Croft para habilitar a validação no meu formulário. Existem muitas maneiras, eu achei interessante experimentar o pacote MADE dele. Crie agora uma nova classe normal chamada NewTodoItemViewModel. Vou mostrar o código agora e explicar pouco a pouco depois:
using MADE.Data.Validation;
using MADE.Data.Validation.Validators;
using MvvmHelpers;
using System;
namespace TodoApp
{
public class NewTodoItemViewModel : ObservableObject
{
private string title;
private string description;
private DateTimeOffset dueDate;
private bool isTitleValid;
private bool isDescriptionValid;
private bool isViewModelValid;
public NewTodoItemViewModel()
{
TitleValidators.Validated += ValidateTitle;
DescriptionValidators.Validated += ValidateDescription;
}
public string Title
{
get => title;
set
{
SetProperty(ref title, value);
}
}
public string Description
{
get => description;
set
{
SetProperty(ref description, value);
}
}
public DateTimeOffset DueDate
{
get => dueDate;
set
{
SetProperty(ref dueDate, value);
}
}
public bool IsTitleValid
{
get => isTitleValid;
set
{
SetProperty(ref isTitleValid, value);
}
}
public bool IsDescriptionValid
{
get => isDescriptionValid;
set
{
SetProperty(ref isDescriptionValid, value);
}
}
public bool IsViewModelValid
{
get => isViewModelValid;
set
{
SetProperty(ref isViewModelValid, value);
}
}
public ValidatorCollection TitleValidators { get; } = new ValidatorCollection() { new RequiredValidator(), new MinLengthValidator(4) };
public ValidatorCollection DescriptionValidators { get; } = new ValidatorCollection() { new RequiredValidator(), new MinLengthValidator(10) };
private void ValidateTitle(object sender, InputValidatedEventArgs args)
{
IsTitleValid = !TitleValidators.IsInvalid;
UpdateIsViewModelValid();
}
private void ValidateDescription(object sender, InputValidatedEventArgs args)
{
IsDescriptionValid = !DescriptionValidators.IsInvalid;
UpdateIsViewModelValid();
}
private void UpdateIsViewModelValid()
{
IsViewModelValid = IsTitleValid && IsDescriptionValid;
}
}
}
Vamos derivar de ObservableObject que vem do pacote Refractored.MvvmHelpers. Isso dará o uso da função SetProperty. Basicamente, faz todas as coisas do InotifyPropertyChanged que você normalmente faria por conta própria. Portanto, esse é o padrão MVVM típico que você faria com qualquer aplicativo WPF/XAML. Nosso ViewModel tem 2 responsabilidades:
- Segurando os dados via Título, Descrição e Data de Vencimento.
- Faça a validação. Para isso temos duas ValidatorCollections. Um (TitleValidators) para verificar se o título é válido e os DescriptionValidators para a descrição. Temos a oportunidade de passar vários validadores ao mesmo tempo. No nosso caso queremos expressar: Ei o Título é obrigatório e tem que ter no mínimo 4 caracteres. O mesmo se aplica à nossa Descrição, mas deve ter pelo menos 10 caracteres. Se quiser saber mais confira os Validadores no repositório. Em nosso construtor também estamos encadeando alguns eventos via TitleValidators.Validated += ValidateTitle; e DescriptionValidators.Validated += ValidateDescription;. Esses eventos são acionados pela biblioteca quando a propriedade subjacente está sendo alterada. Veremos mais adiante no código frontend como isso é feito. Por enquanto, temos que saber que queremos ter a propriedade IsValid para ambas as nossas propriedades. E também queremos um IsViewModelValid geral que só seja verdadeiro quando o título e a descrição forem válidos. Usaremos essa propriedade para alternar o botão Adicionar. Se nosso modelo não for válido, não poderemos adicionar um item. Resumindo: Nossa propriedade está mudando, então todos os validadores em nosso ValidatorCollection são acionados e nós o salvamos em uma propriedade.
Agora a última parte, o código frontend. Temos que envolver nossos componentes com o InputValidator também fornecido pela biblioteca MADE.UI.Controls.Validator.
<ContentDialog
x:Class="TodoApp.AddTodoItemDialog"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:TodoApp"
xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="using:MADE.UI.Controls"
Title="Add new todo item"
PrimaryButtonText="Add"
IsPrimaryButtonEnabled="False"
SecondaryButtonText="Cancel">
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="10" />
<RowDefinition Height="Auto" />
<RowDefinition Height="10" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Title:" />
<controls:InputValidator Grid.Row="0" Grid.Column="2"
Input="{x:Bind titleTextBox.Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Validators="{x:Bind viewModel.TitleValidators}">
<TextBox x:Name="titleTextBox" Text="{x:Bind viewModel.Title}"/>
</controls:InputValidator>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Due Date:" />
<DatePicker Grid.Row="2" Grid.Column="2"/>
<TextBlock Grid.Row="4" Grid.Column="0" Text="Description:" />
<controls:InputValidator Grid.Row="4" Grid.Column="2"
Input="{x:Bind descriptionTextBox.Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Validators="{x:Bind viewModel.DescriptionValidators}">
<TextBox x:Name="descriptionTextBox" AcceptsReturn="True" TextWrapping="Wrap" Text="{x:Bind viewModel.Description}"/>
</controls:InputValidator>
</Grid>
</ContentDialog>
Podemos ver que nosso TextBox foi envolvido pelo InputValidator. O InputValidator está diretamente vinculado à propriedade Text do TextBox. Como você pode ver, também estamos declarando quais validadores serão invocados através da propriedade Validators. A desvantagem dessa abordagem é que, se você precisar de um sinalizador IsValid agregado em todas as suas propriedades, precisará criar uma ValidatorCollection para cada propriedade que tiver. Se você compilar toda a solução novamente e executar o projeto, terá a mesma imagem do topo desta página ??
Com isso concluímos esta parte da série. Implementamos com sucesso um diálogo modal agradável que funciona em todas as plataformas que permite ao usuário inserir informações básicas sobre o item de tarefas que deseja rastrear com nosso pequeno aplicativo.
Qual é o próximo
Agora criamos um novo item de tarefas, mas ele não é exibido em nenhum lugar. Também não podemos interagir ou acompanhar o estado atual. Isso fará parte do nosso próximo episódio da série.