Desenvolvimento

12 dez, 2012

Modificando e deletando o item Tracking com Ingragistics DataGrid

Publicidade

Agora que eu estou de volta desenvolvendo linha de produção de Negócios (LOB), gostaria de compartilhar algumas técnicas e desafios que superei em nosso WPF UI.

Nossa empresa utiliza o produto Infragistics NetAdvantage for WPF para o nosso WPF UI. Estamos muito satisfeitos com o produto, resposta de suporte e atualizações. Atualmente, nós usamos o XamRibbon e o XamOutlookBar no nosso shell Prism; o XamDataChart em nosso painel, e XamDataGrid em nossos formulários de entrada de dados. Nosso aplicativo está em sua infância e, à medida que novos requisitos, funcionalidades e capacidades são adicionados, iremos usar mais os controles dos recursos da suíte NetAdvantage para WPF.

Introdução

Para o meu primeiro artigo, eu pensei que iria abranger um aspecto do uso da XamDataGrid (todos os data grids, na verdade) que é fundamental para aplicativos LOB.

Estou me referindo ao acompanhamento das edições dos usuários (inserções, atualizações e exclusões) enquanto trabalhava em um data grid e, em seguida, fornecendo a capacidade de utilizar a mesma conexão de banco de dados para executar as atualizações e, opcionalmente, realizar as atualizações como uma transação atômica.

Existem casos de uso quando um usuário muda de dados em uma rede de dados, e que essas mudanças são imediatamente refletidas de volta para o banco de dados.

Nosso cenário é aquele em que o projeto exige que o usuário seja capaz de fazer suas edições e depois comitar todas as mudanças de uma só vez. Um exemplo disso seria um caso de uso de master-detail, quando as linhas filhas são associadas com um registro mestre, por exemplo, clientes, pedidos, detalhes do pedido.

Neste exemplo, todas as questões de desenvolvimento, tais como validação de dados, concorrência, lendo ou escrevendo de fato para um banco de dados não fazem parte do código de exemplo, o que permite ao leitor se concentrar em uma técnica para inserções de tracking, atualizações e exclusões.

Requisito XamDataGrid

O Infragistics XamDataGrid requer que a fonte de dados implemente IBindingList para que o data grid suporte a adição de linhas.

Sim, existem técnicas para adicionar dinamicamente os dados para o seu código sem ter que fazer isso, mas eu queria permitir que a rede de dados executasse o seu trabalho de uma forma natural para que o nosso código fonte seja derivado de BindingList<T>.

Inserções de tracking, atualizações e exclusões

Quando o usuário exclui uma linha no XamDataGrid, a mesma é removida da coleção e não é mais visível na UI.

É aqui que o ChangeTrackingBindingList<T> abaixo entra em jogo. Essa classe mantém o controle das linhas deletadas pela adição de uma linha deletada a uma coleção interna e, em seguida, expõe um método (GetDeletedItems) para retornar as linhas deletadas quando necessário. Se você olhar abaixo, você vai ver que eu substituí o método RemoveItem; a implementação adiciona a linha deletada para a coleção interna de linhas deletadas.

Ele também expõe um método (GetChangedItems) para retornar apenas as linhas não-deletadas que foram inseridas ou atualizadas.

namespace GridEditing.Infrastructure {
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;

    /// <summary>
    /// Represents the ChangeTrackingBindingList that tracks changes and deleted items.
    /// </summary>
    /// <typeparam name="T">
    /// T: The type of object to provide change tracking binding list services for.
    /// </typeparam>
    public class ChangeTrackingBindingList<T> : BindingList<T> where T : ITrackDirtyEntity {

        IList<T> _deletedItems = new List<T>();

        /// <summary>
        /// Initializes a new instance of the <see cref="ChangeTrackingBindingList{T}"/> class.
        /// </summary>
        public ChangeTrackingBindingList() {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ChangeTrackingBindingList{T}"/> class.
        /// </summary>
        /// <param name="list">The list.</param>
        public ChangeTrackingBindingList(IList<T> list)
            : base(list) {
        }

        /// <summary>
        /// Gets all items in the collection.
        /// </summary>
        /// <returns>
        ///   <see cref="IEnumerable{T}"/> that contains all items from this collection; 
        ///   includes deleted and non-deleted items.
        /// </returns>
        public IEnumerable<T> GetAllItems() {
            return this.Union(_deletedItems).ToList();
        }

        /// <summary>
        /// Gets items that have been changed in the collection.
        /// </summary>
        /// <returns>
        ///   <see cref="IEnumerable{T}"/> that contains items that have been changed; 
        ///   does not include deleted items.
        /// </returns>
        public IEnumerable<T> GetChangedItems() {
            return this.Where(i => i.IsDirty).ToList();
        }

        /// <summary>
        /// Gets the deleted items.
        /// </summary>
        /// <returns>
        ///   <see cref="IEnumerable{T}"/> that contains all items deleted from this collection.
        /// </returns>
        public IEnumerable<T> GetDeletedItems() {
            return _deletedItems;
        }

        /// <summary>
        /// Clears the items.
        /// </summary>
        protected override void ClearItems() {
            base.ClearItems();
            _deletedItems = new List<T>();
        }

        /// <summary>
        /// Sets the item's MarkedAsDeleted property to <c>true</c>. 
        /// Adds the item to the DeletedItems collection. Removes item from list.
        /// </summary>
        /// <param name="index">The index.</param>
        protected override void RemoveItem(Int32 index) {
            var item = this[index];
            _deletedItems.Add(item);
            base.RemoveItem(index);
        }
    }
}

 

Consumindo o ChangeTrackingBindingList<T>

O MainWindowViewModel abaixo expõe a collection Costumers. A collection é inicializada e os clientes inseridos no constructor. (favor não popule suas coleções em seus construtores, este é apenas o código-demo)

Você vai notar que após carregar os clientes, eu faço um loop através da collection e defino a propriedade IsDirty para falso. Sua classe base para objetos de entidade deve lidar com isso para você, de modo que quando um objeto é populado a partir de um banco de dados, o objeto é retornado da camada de serviço em um estado limpo para fornecer tracking preciso na camada de UI. O exemplo abaixo é mais simplificado com o propósito de mostrar como o modelo de exibição irá processar os dados, uma vez que o usuário salva suas alterações.

O método mais importante abaixo é o SaveExecute, que é chamado quando o usuário clica no botão Salvar. O método CanSaveExecute determina se a collection foi alterada ou não. Qualquer alteração à collection fará com que o botão Salvar seja ativado.

Observação: o código no método SaveExecute estaria realmente sendo executado em uma camada de serviço em 99,9999% do tempo. O método da camada de serviço receberia a ChangeTrackBindingList<T> como um argumento e usaria uma camada de dados para processar as alterações.

Dentro do método SaveExecute, podemos ver o fluxo de trabalho:

  • Os itens deletados são removidos do banco de dados.
  • Em seguida, os itens inseridos ou atualizados são comitados no banco de dados.
  • O modelo de exibição então recarrega os dados inseridos e mudados no data grid.  Esse recarregamento atualiza os timestamps que desempenham um papel na concorrência, e colunas de identidade são populadas depois que um item é inserido e recarregado a partir do banco de dados.
namespace GridEditing {
  using System;
  using System.Diagnostics;
  using System.Linq;
  using System.Windows.Input;
  using GridEditing.Infrastructure;
  using GridEditing.Model;

  public class MainWindowViewModel : ObservableObject {
    ChangeTrackingBindingList<Customer> _customers;
    ICommand _saveCommand;
    Boolean _customersDirty;

    public ChangeTrackingBindingList<Customer> Customers {
      get { return _customers; }
      set {
        _customers = value;
        RaisePropertyChanged("Customers");
      }
    }

    public ICommand SaveCommand {
      get { return _saveCommand ?? (_saveCommand = new RelayCommand(SaveExecute, CanSaveExecute)); }
    }

    public MainWindowViewModel() {
      // load from the service layer
      var list = new ChangeTrackingBindingList<Customer>();
      list.Add(new Customer { FirstName = "Josh", LastName = "Smith", Id = 1 });
      list.Add(new Customer { FirstName = "Sacha", LastName = "Barber", Id = 2 });
      list.Add(new Customer { FirstName = "Brian", LastName = "Lagunas", Id = 3 });
      list.Add(new Customer { FirstName = "Karl", LastName = "Shifflett", Id = 4 });
      foreach (var customer in list) {
        customer.IsDirty = false;
      }
      this.Customers = list;
      this.Customers.ListChanged += (s, e) => _customersDirty = true;
    }

    Boolean CanSaveExecute() {
      return _customersDirty;
    }

    void SaveExecute() {
      // call the service layer, passing the DeleteMarkingBindingList<Customer>
      // the service layer will loop through each item, and either insert it, update it, delete it, 
      // or discard.
      // this is demo code only, but demonstrates the workflow

      foreach (var customer in this.Customers.GetDeletedItems()) {
        // if the Id property is zero then the service layer does not have to do anything
        // if the Id property is greater than zero, the service layer will delete the item.
        // simulate removing from the data base
        Debug.WriteLine(
          String.Format("Customer deleted: {0} {1}", customer.FirstName, customer.LastName));
      }

      foreach (var customer in this.Customers.GetChangedItems()) {
        if (customer.Id == 0) {
          // perform insert in service layer
          customer.Id = this.Customers.Max(c => c.Id) + 1;
          // simulate inserting into the data base
          Debug.WriteLine(
            String.Format("Customer inserted: {0} {1}", customer.FirstName, customer.LastName));
        } else {
          // perform update in service layer
          // simulate updating the data base
          Debug.WriteLine(
            String.Format("Customer updated: {0} {1}", customer.FirstName, customer.LastName));
        }
      }

      // reload the updated records from the database
      // simulate getting all records from the database

      _customersDirty = false;
    }
  }
}

Executando o aplicativo

Quando um spin up é inicialmente feito em um aplicativo, o botão Salvar será desativado, já que não houve inserções, atualizações ou exclusões.

Depois que uma linha é inserida, o botão Salvar é ativado.

Para os dados iniciais, as seguintes alterações foram feitas.

Quando o botão Salvar é clicado, observe a janela de saída de depuração. Cada uma das operações solicitadas é executada; essas operações podem ser facilmente acionadas pelo ChangeTrackingBindingList<T>.

Após o Salvar estar completo, a UI será atualizada conforme abaixo, o campo Id foi populado, e o botão fica desativado novamente.

UI XAML – Dicas de XamDataGrid

O Infragistics XamDataGrid realmente torna fácil para os desenvolvedores entregarem uma experiência completa de edição de dados da rede. Este exemplo nem sequer arranha a superfície das muitas capacidades do data grid. Nos próximos artigos, demonstrarei alguns cenários do mundo real no que diz respeito às capacidades do UI do XamDataGrid; por enquanto, vamos ficar com o tracking das edições de usuários e atualização do banco de dados.

Dicas

1. Por padrão, o XamDataGrid irá definir propriedades da string para nulo se o usuário entra com uma string em branco ou vazia na grade de dados. Esse não é o comportamento que desejo, em vez isso, eu preciso de uma string vazia. Isso é realizado usando um conversor no-op.

Infragistics tornou rápida a aplicação do conversor não-op, definindo a propriedade ValueToTextConverter no XamTextEditor usando um estilo que eu fiz abaixo. Nos métodos Convert e ConvertBack do XamDataGridNullStringPreventionConverter, eu simplesmente retorno o valor. Isso leva o XamDataGrida a definir as propriedades da string para uma string vazia em vez do valor nulo.

2. Dê uma olhada abaixo no XamDataGrid.FieldSettings, você vai notar que CellClickAction foi definido para EnterEditModeIfAllowed. O usuário não tem mais que dar um duplo clique em uma célula para entrar em modo de edição; simplesmente clicando na célula, vai colocá-la em modo de edição.

3. Fazer um campo Id somente de leitura é simples. Dê uma olhada no campo Id no FieldLayout do XamDataGrid. Ao definir colunas Id FieldSettings.AllowEdit para falso, as células são somente leitura.

<Window 
    xmlns:local="clr-namespace:GridEditing"
    xmlns:infrastructure="clr-namespace:GridEditing.Infrastructure"
    x:Class="GridEditing.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Data Grid Editing and Deleting" 
    Height="341" 
    Width="608" 
    ResizeMode="NoResize"
    xmlns:igDP="http://infragistics.com/DataPresenter" 
    xmlns:igEditors="http://infragistics.com/Editors">
  <Window.DataContext>
    <local:MainWindowViewModel />
  </Window.DataContext>
  <Window.Resources>
    <infrastructure:XamDataGridNullStringPreventionConverter 
      x:Key="XamDataGridNullStringPreventionConverter" />
  </Window.Resources>
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto" />
      <RowDefinition Height="*" />
      <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <TextBlock Text="Infragistics Data Grid Editing and Deleting" Margin="11" FontSize="18" />
    <igDP:XamDataGrid 
      Grid.Row="1" Margin="11" 
      DataSource="{Binding Path=Customers}" IsNestedDataDisplayEnabled="False">
      <igDP:XamDataGrid.Resources>
        <Style TargetType="{x:Type TextBox}" BasedOn="{StaticResource {x:Type TextBox}}" >
          <Setter Property="Margin" Value="0" />
        </Style>
        <!--this style is required to prevent null strings from being sent to the model-->
        <Style TargetType="{x:Type igEditors:XamTextEditor}" >
          <Setter 
            Property="ValueToTextConverter" 
            Value="{StaticResource XamDataGridNullStringPreventionConverter}" />
        </Style>
      </igDP:XamDataGrid.Resources>
      <igDP:XamDataGrid.FieldSettings>
        <igDP:FieldSettings CellClickAction="EnterEditModeIfAllowed" />
      </igDP:XamDataGrid.FieldSettings>
      <igDP:XamDataGrid.FieldLayoutSettings>
        <igDP:FieldLayoutSettings 
                    SupportDataErrorInfo="None"
                    DataErrorDisplayMode="None" 
                    AutoGenerateFields="False"
                    AddNewRecordLocation="OnBottom" 
                    AllowAddNew="True" 
                    AllowClipboardOperations="All" 
                    AllowDelete="True" 
                    ExpansionIndicatorDisplayMode="Never" />
      </igDP:XamDataGrid.FieldLayoutSettings>
      <igDP:XamDataGrid.FieldLayouts>
        <igDP:FieldLayout>
          <igDP:FieldLayout.SortedFields>
            <igDP:FieldSortDescription FieldName="Id" Direction="Ascending" />
          </igDP:FieldLayout.SortedFields>
          <igDP:Field Name="Id">
            <igDP:Field.Settings>
              <igDP:FieldSettings AllowEdit="False" />
            </igDP:Field.Settings>
          </igDP:Field>
          <igDP:Field Name="FirstName" Width="200" />
          <igDP:Field Name="LastName" Width="200" />
        </igDP:FieldLayout>
      </igDP:XamDataGrid.FieldLayouts>
    </igDP:XamDataGrid>

    <Button 
      Width="65" Margin="7" Grid.Row="2" Content="Save" 
      Command="{Binding Path=SaveCommand}" HorizontalAlignment="Right"/>

  </Grid>
</Window>

Download

Depois de fazer o download, altere a extensão de. zip.DOC para .zip. Essa é uma exigência do WordPress.

Download Demo Application (25K).

Requisitos

Para executar o aplicativo de demonstração, você vai precisar obter o Infragistics NetAdvantage para WPF. Uma versão demo está disponível aqui.

Encerrar

Existem frameworks empresariais, como CSLA, que possuem objetos rastreáveis e lidam com os cenários acima que mostrei. Atualmente, eu prefiro a solução mais simples possível e usar o acima mostrado para alterações do usuário de tracking dentro do XamDataGrid.

Tenha um ótimo dia!

***

Texto original disponível em http://karlshifflett.wordpress.com/2012/07/02/change-and-deleted-item-tracking-within-the-infragistics-datagrid/