Plataforma UNO - Criando um App ToDo - parte 5

Imagem de capa Plataforma UNO - Criando um App ToDo - parte 5

Plataforma UNO - Plataforma UNO - Part 5

Esta é a última parte da nossa série para encerrar todas as coisas. Implementaremos o comportamento de arrastar e soltar e preservaremos e carregaremos o estado para que possamos continuar de onde paramos. Como sempre aqui o resultado primeiro e depois passamos pelas etapas simples:

Arraste e Solte

A parte boa primeiro nosso Swimlane ou mais detalhadamente nosso ListView tem tudo o que precisamos para tornar possível o arrastar e soltar. Então vamos estender o ListView com as propriedades necessárias com as seguintes propriedades:

Swimlane.xaml.cs

<ListView  x:Name="itemListView"
                   AllowDrop="True"
                   CanDragItems="True"
  • AllowDrop - Permitimos que os elementos sejam descartados em nosso ListView
  • CanDragItems - Nós permitimos que o usuário arraste seus itens do ListView. No nosso caso, os itens Todo

Sem essas duas propriedades podemos implementar o que queremos e nunca teremos sucesso. Então os requisitos estão feitos: Confira! A próxima parte será lidar com os eventos em si. Temos que fornecer 4 deles. Vou analisá-los em detalhes:

DragOver="SetDragOverIcon"
DragItemsStarting="SetDragItem"
Drop="DropItem"
DragItemsCompleted="UpdateList"

Arraste Sobre ( DragOver )

O mais simples no início. Aproveitaremos o evento arraste sobre para definir o ícone quando movermos o ícone de uma pista para outra:

private  void  SetDragOverIcon(object sender, DragEventArgs e)
{
    e.AcceptedOperation = DataPackageOperation.Move;
}

Usamos o ícone Mover. Acho que isso não precisa de mais explicações.

Arrastar itens começando (DragItemsStarting)

Este evento é usado para indicar que um item do nosso ListView começou a ser arrastado. Usaremos este evento para definir as informações sobre o item arrastado. A ideia por trás do código a seguir é que salvamos o ID do item Todo que arrastamos. Lembre-se: Todos os Swimlanes conhecem, em teoria, todos os Todo-items. Podemos alavancar isso. Ah sim, usamos o Id porque é a maneira mais fácil. Você verá na segunda. E se você se perguntar Id? Qual identificação? Sim, adicionaremos um ID ao nosso objeto de domínio Todo:

public  class  Todo
{
     public Guid Id { get; set; } = Guid.NewGuid();
     
     public  string Title { get; set; }

Agora que temos isso, vamos escrever o código para o evento Arrastar itens começando (DragItemsStarting):

private  void  SetDragItem(object sender, DragItemsStartingEventArgs e)
{
    // We only have one item we can drag around (in theory we can also allow multi-select)
    var draggedItem = e.Items.First() as Todo;
    
    // We set the id of the current dragged item
    e.Data.SetText(draggedItem.Id.ToString());
}

Derrubar ( Drop )

Conforme discutido anteriormente: A Swimlane tem acesso a todos os itens. Portanto, obtemos o item com o Id e definimos nosso Estado para seu Estado. Lembra como introduzimos AdvancedCollectionView na última parte? Temos que atualizar a visão para atualizar o filtro. Para fazer isso, refatore o AdvancedCollectionView em um novo campo. Deve ficar assim:

private AdvancedCollectionView view;

private  void  SetFilter(FrameworkElement sender, DataContextChangedEventArgs args)
{
      view = new AdvancedCollectionView(((MainPageViewModel)this.DataContext).TodoItems, true);
      view.Filter = item => ((Todo)item).KanbanState == State;
      itemListView.ItemsSource = view;
}

A coisa legal sobre o DragEventArgs é que eles mantêm os dados que definimos no DragItemsStarting. Assim, obteremos o ID que salvamos antes e procuraremos o item Todo com exatamente esse ID. Se partirmos do State New e o movermos para o InProgress, será o InProgress Swimlane que receberá o evento Drop.

private  async  void  DropItem(object sender, DragEventArgs e)
{
    var todoId = Guid.Parse(await e.DataView.GetTextAsync());
    var todo = ((MainPageViewModel)DataContext).TodoItems.Single(t => t.Id == todoId);
    todo.KanbanState = State;
    view.Refresh();
}

A view.Refresh levará à atualização da lista. Agora só falta um pequeno detalhe: temos que atualizar a lista original.

Arrastar itens concluídos (DragItemsCompleted)

Como dito anteriormente, quando movemos um item de New para InProgress, a Swimlane InProgress receberá o evento Drop quando "droparmos" o item. Mas também a New Swimlane receberá um evento: DragItemsCompleted. Usamos para atualizar o filtro original:

private  void  UpdateList(object sender, DragItemsCompletedEventArgs e)
{
    view.Refresh();
}

Puxa terminamos! Aperte F5 e execute o projeto! Crie um item e tente arrastá-lo e soltá-lo de uma pista para outra... E? Não funciona!

Painel de pilha (StackPanel)

O problema aqui é o StackPanel. Vamos demonstrar algo. Basta adicionar um BackgroundBrush ao ListView assim:

<ListView  x:Name="itemListView"  Background="Beige"
             AllowDrop="True"

O problema é que nosso ListView é tão grande quanto nosso ListViewItems. Mas isso significa que quando temos uma lista vazia, não há "espaço" para soltar um item. Podemos consertar isso facilmente. Basta usar um Grid em vez de nosso StackPanel:

<Grid  MinWidth="200"  MinHeight="400"  BorderBrush="DarkOliveGreen"  BorderThickness="2">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition  Height="10" />
        <RowDefinition Height="*" />
   </Grid.RowDefinitions>
   
   <TextBlock Text="{x:Bind State}" HorizontalAlignment="Center" Grid.Row="0"></TextBlock>
   <ListView x:Name="itemListView" Background="Beige" Grid.Row="2"
               AllowDrop="True"

Com Height="*" deixamos o ListView preencher completamente o espaço disponível. Aqui como fica:

E se você tentar agora, tudo funciona como deveria. Vivaaaa! ???????

Só nos resta uma coisa a fazer: temos que cuidar do manejo do nosso estado!

Preservar e carregar o estado

Agora aqui está o legal, só escrevemos a lógica para salvar o estado uma vez e funciona em todos os lugares! Não importa se android ou web. Podemos facilmente salvar o estado. Basicamente, vamos serializar nossa lista com json para preservar o estado e desserializar quando carregarmos em nosso aplicativo. Para isso, instalamos o pacote nuget Newtonsoft.Json. Você pode adicionar isso a todos os projetos produtivos.

  • Loaded - Este evento será gerado assim que a página estiver disponível. Aqui queremos carregar o estado, se estiver disponível.
  • LostFocus será gerado toda vez que clicarmos fora, por exemplo, ou algum pop-up aparecer na página como nosso AddTodoItemDialog. Mas o mais importante, ele será gerado quando fecharmos nosso aplicativo!
public  MainPage()
{ 
    InitializeComponent();
    DataContext = new MainPageViewModel();
    addItemButton.TodoItemCreated += (o, item) => ((MainPageViewModel)DataContext).TodoItems.Add(item);
    
    Loaded += RecoverState;
    LostFocus += (s, e) => SaveState();
}

private  async  void  SaveState()
{
     var folder = ApplicationData.Current.LocalFolder; // Get the local folder (on WASM IndexedDb is used)
     var file = await folder.CreateFileAsync("todoapp.json", CreationCollisionOption.OpenIfExists); // Create the file or open it
     await FileIO.WriteTextAsync(file, JsonConvert.SerializeObject(DataContext)); // Serialize our ViewModel and save it to file
}

private  async  void  RecoverState(object sender, RoutedEventArgs e)
{
    var folder = ApplicationData.Current.LocalFolder;
    var file = await folder.TryGetItemAsync("todoapp.json"); // Try to grab the file if it exists
    if (file != null)
    {
        // Read the content of the file and deserialize into our MainPageViewModel object
        var text = await FileIO.ReadTextAsync(file as IStorageFile);
        var viewmodel = JsonConvert.DeserializeObject<MainPageViewModel>(text);
        if (viewmodel?.TodoItems.Any() == true)
        {
            // if we have any data just set the DataContext to it
            DataContext = viewmodel;
        }
    }
}

Super direto! Conseguimos! Construímos um pequeno aplicativo de tarefas. Claro que poderia ser mais sexy, mas essa parte pode ser feita mais tarde. Por enquanto é importante que tenhamos um aplicativo de tarefas que possa ser usado em várias plataformas com a mesma base de código-fonte. Isso é simplesmente incrível.

Resumo

Agora o que eu aprendi ao longo dos últimos 5 episódios da série. Qual é a minha experiência com a Plataforma Uno em geral?

Eu inicialmente pensei que ter esse projeto compartilhado poderia ser complicado de trabalhar, mas na verdade não era tão horrível assim. Claro que ainda há o problema de instalar um pacote nuget para todos os Heads, mas mesmo isso "sumiu" com os novos modelos .net6. Na série eu usei o modelo "normal" onde a cabeça UWP ainda está rodando .net core 3.1 ao invés de .net6. Isso significa que não podemos usar o truque Directory.Build.Props, mas eles estão trabalhando nisso.

Uma coisa menor foi que às vezes as mensagens de erro são confusas. Isso pode ser principalmente porque eu não trabalhei muito com Xamarin e WPF ultimamente. De qualquer forma, se você tiver um problema no arquivo de código xaml, receberá muitos erros na saída e realmente precisará ver o que está acontecendo. Mais frequentemente, quando eu tinha um arquivo xaml malformado, recebo > 20 erros e um deles é algo assim: CS0103 O nome 'InitializeComponent' não existe no contexto atual.

O que eu realmente gostei é a documentação da Plataforma Uno. Existem muitas amostras que você pode utilizar. Especialmente como iniciante, isso é realmente importante porque a curva de aprendizado é bastante íngreme no início. E também a maioria das perguntas e respostas do UWP ou WinUI Stackoverflow são verdadeiras para a plataforma Uno, o que faz sentido à medida que vão na mesma direção. Também fiquei muito fascinado na primeira vez que iniciei várias plataformas e funcionou da mesma maneira.

Em suma, se você está acostumado com WPF e/ou trabalha com XAML, você se sente em casa. Espero que você também possa fazer algo com você daquela pequena minissérie. Apreciei muito.

Recursos

  • O repositório do github para este Todo-App: aqu
  • Site oficial da Plataforma UNO: aqui