Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
por Scott Mitchell
No tutorial anterior, aprendemos como aplicar o cache na Camada de Apresentação. Neste tutorial, aprendemos como aproveitar nossa arquitetura em camadas para armazenar dados em cache na camada de lógica de negócios. Fazemos isso estendendo a arquitetura para incluir uma camada de cache.
Introdução
Como vimos no tutorial anterior, armazenar em cache os dados de ObjectDataSource é tão simples quanto definir algumas propriedades. Infelizmente, o ObjectDataSource aplica cache na camada de apresentação, que combina firmemente as políticas de cache com a página ASP.NET. Uma das razões para criar uma arquitetura em camadas é permitir que esses acoplamentos sejam quebrados. A Camada de Lógica de Negócios, por exemplo, separa a lógica de negócios das páginas de ASP.NET, enquanto a Camada de Acesso a Dados separa os detalhes de acesso aos dados. Essa dissociação da lógica de negócios e dos detalhes de acesso aos dados é preferível, em parte, porque torna o sistema mais legível, mais fácil de manter e mais flexível para mudar. Ele também permite o conhecimento do domínio e divisão de trabalho: um desenvolvedor que trabalha na camada de apresentação não precisa estar familiarizado com os detalhes do banco de dados para fazer seu trabalho. Desacoplar a política de cache da camada de apresentação oferece benefícios semelhantes.
Neste tutorial, aumentaremos nossa arquitetura para incluir uma camada de cache (ou CL) que emprega nossa política de cache. A camada de cache incluirá uma ProductsCL
classe que fornece acesso às informações do produto com métodos como GetProducts()
, GetProductsByCategoryID(categoryID)
, e assim por diante, que, quando invocados, tentarão primeiro recuperar os dados do cache. Se o cache estiver vazio, esses métodos invocarão o método apropriado ProductsBLL
na BLL, que por sua vez obterá os dados da DAL. Os ProductsCL
métodos armazenam em cache os dados recuperados da BLL antes de retorná-los.
Como mostra a Figura 1, a CL reside entre as camadas de apresentação e lógica de negócios.
Figura 1: A camada de cache (CL) é outra camada em nossa arquitetura
Etapa 1: Criando as classes da camada de cache
Neste tutorial, vamos criar um CL muito simples com uma única classe ProductsCL
que tem apenas um punhado de métodos. A criação de uma camada de cache completa para todo o aplicativo exigiria a criação de CategoriesCL
, EmployeesCL
e SuppliersCL
classes, e fornecer um método nessas classes da camada de caching para cada acesso a dados ou método de modificação na BLL. Tal como acontece com a BLL e a DAL, a camada de cache deve idealmente ser implementada como um projeto de biblioteca de classes separado; no entanto, vamos implementá-lo como uma classe na App_Code
pasta.
Para separar mais claramente as classes CL das classes DAL e BLL, vamos criar uma nova subpasta na App_Code
pasta. Clique com o botão direito do App_Code
mouse na pasta no Gerenciador de Soluções, escolha Nova Pasta e nomeie a nova pasta CL
. Depois de criar essa pasta, adicione a ela uma nova classe chamada ProductsCL.vb
.
Figura 2: Adicionar uma nova pasta nomeada CL
e uma classe chamada ProductsCL.vb
A ProductsCL
classe deve incluir o mesmo conjunto de métodos de acesso e modificação de dados encontrados em sua classe correspondente Business Logic Layer (ProductsBLL
). Em vez de criar todos esses métodos, vamos apenas construir um par aqui para ter uma ideia dos padrões usados pelo CL. Em particular, adicionaremos o método GetProducts()
e o método GetProductsByCategoryID(categoryID)
na Etapa 3 e uma sobrecarga UpdateProduct
na Etapa 4. Você pode adicionar os métodos restantes ProductsCL
e as classes CategoriesCL
, EmployeesCL
e SuppliersCL
à sua vontade.
Etapa 2: Leitura e gravação no cache de dados
O recurso de cache ObjectDataSource explorado no tutorial anterior usa internamente o cache de dados ASP.NET para armazenar os dados recuperados da BLL. O cache de dados também pode ser acessado programaticamente a partir de classes code-behind de páginas ASP.NET ou das classes na arquitetura da aplicação web. Para ler e gravar no cache de dados a partir de uma classe code-behind de uma página ASP.NET, use o seguinte padrão:
' Read from the cache
Dim value as Object = Cache("key")
' Add a new item to the cache
Cache("key") = value
Cache.Insert(key, value)
Cache.Insert(key, value, CacheDependency)
Cache.Insert(key, value, CacheDependency, DateTime, TimeSpan)
O Cache
método de classe s Insert
tem uma série de sobrecargas.
Cache("key") = value
e Cache.Insert(key, value)
são sinônimos, e ambos adicionam um item ao cache usando a chave especificada sem um prazo de validade definido. Normalmente, queremos especificar uma expiração ao adicionar um item ao cache, seja como uma dependência, uma expiração baseada no tempo ou ambas. Use uma das sobrecargas de outro Insert
método para fornecer informações de expiração baseadas em dependência ou tempo.
Os métodos da camada de cache precisam primeiro verificar se os dados solicitados estão no cache e, em caso afirmativo, retorná-los de lá. Se os dados solicitados não estiverem no cache, o método BLL apropriado precisará ser invocado. Seu valor de retorno deve ser armazenado em cache e, em seguida, retornado, como ilustra o diagrama de sequência a seguir.
Figura 3: Os métodos da camada de cache retornam dados do cache, se estiverem disponíveis
A sequência representada na Figura 3 é realizada nas classes CL usando o seguinte padrão:
Dim instance As Type = TryCast(Cache("key"), Type)
If instance Is Nothing Then
instance = BllMethodToGetInstance()
Cache.Insert(key, instance, ...)
End If
Return instance
Aqui, Type é o tipo de dados que estão sendo armazenados no cache Northwind.ProductsDataTable
, por exemplo, enquanto a chave é a chave que identifica exclusivamente o item de cache. Se o item com a chave especificada não estiver no cache, a instância será Nothing
e os dados serão recuperados do método BLL apropriado e adicionados ao cache. Quando Return instance
é alcançado, a instância contém uma referência aos dados, seja do cache ou extraídos da BLL.
Certifique-se de usar o padrão acima ao acessar dados do cache. O padrão a seguir, que, à primeira vista, parece equivalente, contém uma diferença sutil que introduz uma condição de raça. As condições de corrida são difíceis de depurar porque se revelam esporadicamente e são difíceis de reproduzir.
If Cache("key") Is Nothing Then
Cache.Insert(key, BllMethodToGetInstance(), ...)
End If
Return Cache("key")
A diferença neste segundo trecho de código incorreto é que, em vez de armazenar uma referência ao item armazenado em cache em uma variável local, o cache de dados é acessado diretamente na instrução condicional e no Return
. Imagine que quando este código é atingido, Cache("key")
não é Nothing
, mas antes que a instrução Return
seja alcançada, o sistema remove a chave do cache. Neste caso raro, o código retornará Nothing
em vez de um objeto do tipo esperado.
Observação
O cache de dados é thread-safe, portanto, você não precisa sincronizar o acesso ao thread para leituras ou gravações simples. No entanto, se você precisar executar várias operações em dados no cache que precisam ser atômicos, você é responsável por implementar um bloqueio ou algum outro mecanismo para garantir a segurança do thread. Consulte Sincronizando o acesso ao cache de ASP.NET para obter mais informações.
Um item pode ser removido programaticamente do cache de dados usando o Remove
método assim:
Cache.Remove(key)
Etapa 3: Retornar informações do produto daProductsCL
classe
Para este tutorial, vamos implementar dois métodos para retornar informações do produto da ProductsCL
classe: GetProducts()
e GetProductsByCategoryID(categoryID)
. Como com a ProductsBL
classe na camada de lógica de negócios, o GetProducts()
método na CL retorna informações sobre todos os produtos como um Northwind.ProductsDataTable
objeto, enquanto GetProductsByCategoryID(categoryID)
retorna todos os produtos de uma categoria especificada.
O código a seguir mostra uma parte dos métodos na ProductsCL
classe:
<System.ComponentModel.DataObject()> _
Public Class ProductsCL
Private _productsAPI As ProductsBLL = Nothing
Protected ReadOnly Property API() As ProductsBLL
Get
If _productsAPI Is Nothing Then
_productsAPI = New ProductsBLL()
End If
Return _productsAPI
End Get
End Property
<System.ComponentModel.DataObjectMethodAttribute _
(DataObjectMethodType.Select, True)> _
Public Function GetProducts() As Northwind.ProductsDataTable
Const rawKey As String = "Products"
' See if the item is in the cache
Dim products As Northwind.ProductsDataTable = _
TryCast(GetCacheItem(rawKey), Northwind.ProductsDataTable)
If products Is Nothing Then
' Item not found in cache - retrieve it and insert it into the cache
products = API.GetProducts()
AddCacheItem(rawKey, products)
End If
Return products
End Function
<System.ComponentModel.DataObjectMethodAttribute _
(DataObjectMethodType.Select, False)> _
Public Function GetProductsByCategoryID(ByVal categoryID As Integer) _
As Northwind.ProductsDataTable
If (categoryID < 0) Then
Return GetProducts()
Else
Dim rawKey As String = String.Concat("ProductsByCategory-", categoryID)
' See if the item is in the cache
Dim products As Northwind.ProductsDataTable = _
TryCast(GetCacheItem(rawKey), Northwind.ProductsDataTable)
If products Is Nothing Then
' Item not found in cache - retrieve it and insert it into the cache
products = API.GetProductsByCategoryID(categoryID)
AddCacheItem(rawKey, products)
End If
Return products
End If
End Function
End Class
Primeiro, observe os DataObject
atributos e DataObjectMethodAttribute
aplicados à classe e aos métodos. Esses atributos fornecem informações para o assistente de ObjectDataSource, indicando quais classes e métodos devem aparecer nas etapas do assistente. Como as classes e métodos CL serão acessados de um ObjectDataSource na camada de apresentação, adicionei esses atributos para aprimorar a experiência em tempo de design. Consulte novamente o tutorial Criando uma camada de lógica de negócios para obter uma descrição mais completa sobre esses atributos e seus efeitos.
Nos métodos GetProducts()
e GetProductsByCategoryID(categoryID)
, os dados retornados pelo método GetCacheItem(key)
são atribuídos a uma variável local. O GetCacheItem(key)
método, que examinaremos em breve, retorna um item específico do cache com base na chave especificada. Se esses dados não forem encontrados no cache, eles serão recuperados do método de classe correspondente ProductsBLL
e, em seguida, adicionados ao cache usando o AddCacheItem(key, value)
método.
Os métodos GetCacheItem(key)
e AddCacheItem(key, value)
fazem interface com o cache de dados, lendo e gravando valores, respectivamente. O GetCacheItem(key)
método é o mais simples dos dois. Ele simplesmente retorna o valor da classe Cache usando a chave passada:
Private Function GetCacheItem(ByVal rawKey As String) As Object
Return HttpRuntime.Cache(GetCacheKey(rawKey))
End Function
Private ReadOnly MasterCacheKeyArray() As String = {"ProductsCache"}
Private Function GetCacheKey(ByVal cacheKey As String) As String
Return String.Concat(MasterCacheKeyArray(0), "-", cacheKey)
End Function
GetCacheItem(key)
não usa o valor da chave como fornecido, mas chama o GetCacheKey(key)
método, que retorna a chave anexada com ProductsCache-. O MasterCacheKeyArray
, que contém a cadeia de caracteres ProductsCache, também é usado pelo AddCacheItem(key, value)
método, como veremos momentaneamente.
A partir de uma classe code-behind de uma página ASP.NET, o cache de dados pode ser acedido utilizando a propriedade Page
da classe Cache
, e permite sintaxes como , conforme discutido na Etapa 2. A partir de uma classe dentro da arquitetura, o cache de dados pode ser acedido quer usando HttpRuntime.Cache
quer HttpContext.Current.Cache
. A entrada de blog de Peter JohnsonHttpRuntime.Cache vs. HttpContext.Current.Cache observa a ligeira vantagem de desempenho em usar HttpRuntime
em vez de HttpContext.Current
; consequentemente, ProductsCL
usa HttpRuntime
.
Observação
Se sua arquitetura for implementada System.Web
usando projetos da Biblioteca de Classes, você precisará adicionar uma referência ao assembly para usar as HttpRuntime
classes e HttpContext
.
Se o item não for encontrado no cache, os ProductsCL
métodos da classe s obterão os dados da BLL e os adicionarão ao cache usando o AddCacheItem(key, value)
método. Para agregar valor ao cache, podemos usar o seguinte código, que usa um prazo de validade de 60 segundos:
Const CacheDuration As Double = 60.0
Private Sub AddCacheItem(ByVal rawKey As String, ByVal value As Object)
DataCache.Insert(GetCacheKey(rawKey), value, Nothing, _
DateTime.Now.AddSeconds(CacheDuration), _
System.Web.Caching.Cache.NoSlidingExpiration)
End Sub
DateTime.Now.AddSeconds(CacheDuration)
especifica a expiração baseada no tempo 60 segundos no futuro, enquanto System.Web.Caching.Cache.NoSlidingExpiration
indica que não há expiração deslizante. Embora essa Insert
sobrecarga de método tenha parâmetros de entrada para uma expiração absoluta e deslizante, você só pode fornecer um dos dois. Se você tentar especificar um tempo absoluto e um intervalo de tempo, o Insert
método lançará uma ArgumentException
exceção.
Observação
Esta aplicação do AddCacheItem(key, value)
método apresenta atualmente algumas deficiências. Abordaremos e superaremos esses problemas na Etapa 4.
Etapa 4: Invalidando o cache quando os dados são modificados por meio da arquitetura
Juntamente com os métodos de recuperação de dados, a camada de cache precisa fornecer os mesmos métodos que a BLL para inserir, atualizar e excluir dados. Os métodos de modificação de dados CL não modificam os dados armazenados em cache, mas chamam o método de modificação de dados correspondente da BLL e, em seguida, invalidam o cache. Como vimos no tutorial anterior, esse é o mesmo comportamento que o ObjectDataSource aplica quando seus recursos de cache são habilitados e seus Insert
métodos , Update
ou Delete
são invocados.
A sobrecarga a seguir UpdateProduct
ilustra como implementar os métodos de modificação de dados na CL:
<DataObjectMethodAttribute(DataObjectMethodType.Update, False)> _
Public Function UpdateProduct(productName As String, _
unitPrice As Nullable(Of Decimal), productID As Integer) _
As Boolean
Dim result As Boolean = API.UpdateProduct(productName, unitPrice, productID)
' TODO: Invalidate the cache
Return result
End Function
O método Business Logic Layer de modificação de dados apropriado é invocado, mas antes que sua resposta seja retornada, precisamos invalidar o cache. Infelizmente, invalidar o cache não é simples porque as ProductsCL
classes s GetProducts()
e GetProductsByCategoryID(categoryID)
métodos adicionam itens ao cache com chaves diferentes, e o GetProductsByCategoryID(categoryID)
método adiciona um item de cache diferente para cada categoryID exclusivo.
Ao invalidar o cache, precisamos remover todos os itens que podem ter sido adicionados pela ProductsCL
classe. Isso pode ser feito associando uma dependência de cache com cada item adicionado ao cache no AddCacheItem(key, value)
método. Em geral, uma dependência de cache pode ser outro item no cache, um arquivo no sistema de arquivos ou dados de um banco de dados do Microsoft SQL Server. Quando a dependência é alterada ou removida do cache, os itens de cache aos quais ela está associada são automaticamente removidos do cache. Para este tutorial, queremos criar um item adicional no cache que sirva como uma dependência de cache para todos os itens adicionados por meio da ProductsCL
classe. Dessa forma, todos esses itens podem ser removidos do cache simplesmente removendo a dependência do cache.
Vamos atualizar o AddCacheItem(key, value)
método para que cada item adicionado ao cache por meio desse método seja associado a uma única dependência de cache:
Private Sub AddCacheItem(ByVal rawKey As String, ByVal value As Object)
Dim DataCache As System.Web.Caching.Cache = HttpRuntime.Cache
' Make sure MasterCacheKeyArray[0] is in the cache - if not, add it
If DataCache(MasterCacheKeyArray(0)) Is Nothing Then
DataCache(MasterCacheKeyArray(0)) = DateTime.Now
End If
' Add a CacheDependency
Dim dependency As New Caching.CacheDependency(Nothing, MasterCacheKeyArray) _
DataCache.Insert(GetCacheKey(rawKey), value, dependency, _
DateTime.Now.AddSeconds(CacheDuration), _
System.Web.Caching.Cache.NoSlidingExpiration)
End Sub
MasterCacheKeyArray
é uma matriz de cadeia de caracteres que contém um único valor, ProductsCache. Primeiro, um item de cache é adicionado ao cache e atribuído a data e hora atuais. Se o item de cache já existir, ele será atualizado. Em seguida, uma dependência de cache é criada. O CacheDependency
construtor de classe s tem um número de sobrecargas, mas o que está sendo usado aqui espera duas String
entradas de matriz. O primeiro especifica o conjunto de arquivos a serem usados como dependências. Como não queremos usar nenhuma dependência baseada em ficheiro, um valor de Nothing
é utilizado para o primeiro parâmetro de entrada. O segundo parâmetro de entrada especifica o conjunto de chaves de cache a serem usadas como dependências. Aqui especificamos nossa dependência única, MasterCacheKeyArray
. O CacheDependency
é então passado para o Insert
método.
Com essa modificação para AddCacheItem(key, value)
, invalidar o cache é tão simples quanto remover a dependência.
<DataObjectMethodAttribute(DataObjectMethodType.Update, False)> _
Public Function UpdateProduct(ByVal productName As String, _
ByVal unitPrice As Nullable(Of Decimal), ByVal productID As Integer) _
As Boolean
Dim result As Boolean = API.UpdateProduct(productName, unitPrice, productID)
' Invalidate the cache
InvalidateCache()
Return result
End Function
Public Sub InvalidateCache()
' Remove the cache dependency
HttpRuntime.Cache.Remove(MasterCacheKeyArray(0))
End Sub
Etapa 5: Chamando a camada de cache da camada de apresentação
As classes e métodos do Caching Layer podem ser usados para trabalhar com dados usando as técnicas que examinamos ao longo destes tutoriais. Para ilustrar o trabalho com dados armazenados em cache, salve as suas alterações na classe ProductsCL
e, em seguida, abra a página FromTheArchitecture.aspx
na pasta Caching
e adicione um GridView. A partir da tag inteligente do GridView, crie um novo ObjectDataSource. Na primeira etapa do assistente, você deve ver a ProductsCL
classe como uma das opções da lista suspensa.
Figura 4: A ProductsCL
classe está incluída na lista de Drop-Down de objetos de negócios (Clique para visualizar a imagem em tamanho real)
Depois de selecionar ProductsCL
, clique em Avançar. A lista suspensa na guia SELECT tem dois itens - GetProducts()
e GetProductsByCategoryID(categoryID)
a guia UPDATE tem a única UpdateProduct
sobrecarga. Escolha o GetProducts()
método na guia SELECT e o UpdateProducts
método na guia UPDATE e clique em Finish.
Figura 5: Os ProductsCL
métodos de classe s estão listados nas listas de Drop-Down (Clique para visualizar a imagem em tamanho real)
Depois de concluir o assistente, o Visual Studio definirá a propriedade s OldValuesParameterFormatString
de ObjectDataSource como original_{0}
e adicionará os campos apropriados ao GridView. Altere a OldValuesParameterFormatString
propriedade de volta para seu valor {0}
padrão e configure o GridView para oferecer suporte à paginação, classificação e edição. Como a UploadProducts
sobrecarga usada pela CL aceita apenas o nome e o preço do produto editado, limite o GridView para que apenas esses campos sejam editáveis.
No tutorial anterior, definimos um GridView para incluir campos para ProductName
, CategoryName
e UnitPrice
. Sinta-se à vontade para replicar essa formatação e estrutura, caso em que a sua marcação declarativa do GridView e do ObjectDataSource deve ser semelhante à seguinte:
<asp:GridView ID="Products" runat="server" AutoGenerateColumns="False"
DataKeyNames="ProductID" DataSourceID="ProductsDataSource"
AllowPaging="True" AllowSorting="True">
<Columns>
<asp:CommandField ShowEditButton="True" />
<asp:TemplateField HeaderText="Product" SortExpression="ProductName">
<EditItemTemplate>
<asp:TextBox ID="ProductName" runat="server"
Text='<%# Bind("ProductName") %>' />
<asp:RequiredFieldValidator ID="RequiredFieldValidator1"
ControlToValidate="ProductName" Display="Dynamic"
ErrorMessage="You must provide a name for the product."
SetFocusOnError="True"
runat="server">*</asp:RequiredFieldValidator>
</EditItemTemplate>
<ItemTemplate>
<asp:Label ID="Label2" runat="server"
Text='<%# Bind("ProductName") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:BoundField DataField="CategoryName" HeaderText="Category"
ReadOnly="True" SortExpression="CategoryName" />
<asp:TemplateField HeaderText="Price" SortExpression="UnitPrice">
<EditItemTemplate>
$<asp:TextBox ID="UnitPrice" runat="server" Columns="8"
Text='<%# Bind("UnitPrice", "{0:N2}") %>'></asp:TextBox>
<asp:CompareValidator ID="CompareValidator1" runat="server"
ControlToValidate="UnitPrice" Display="Dynamic"
ErrorMessage="You must enter a valid currency value with
no currency symbols. Also, the value must be greater than
or equal to zero."
Operator="GreaterThanEqual" SetFocusOnError="True"
Type="Currency" ValueToCompare="0">*</asp:CompareValidator>
</EditItemTemplate>
<ItemStyle HorizontalAlign="Right" />
<ItemTemplate>
<asp:Label ID="Label1" runat="server"
Text='<%# Bind("UnitPrice", "{0:c}") %>' />
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
<asp:ObjectDataSource ID="ProductsDataSource" runat="server"
OldValuesParameterFormatString="{0}" SelectMethod="GetProducts"
TypeName="ProductsCL" UpdateMethod="UpdateProduct">
<UpdateParameters>
<asp:Parameter Name="productName" Type="String" />
<asp:Parameter Name="unitPrice" Type="Decimal" />
<asp:Parameter Name="productID" Type="Int32" />
</UpdateParameters>
</asp:ObjectDataSource>
Neste ponto, temos uma página que usa a camada de cache. Para ver o cache em ação, defina pontos de interrupção nas ProductsCL
classes s GetProducts()
e UpdateProduct
métodos. Visite a página em um navegador e percorra o código ao classificar e paginar para ver os dados extraídos do cache. Em seguida, atualize um registro e observe que o cache é invalidado e, consequentemente, ele é recuperado da BLL quando os dados são redirecionados para o GridView.
Observação
A camada de cache fornecida no download que acompanha este artigo não está completa. Ele contém apenas uma classe, ProductsCL
que só ostenta um punhado de métodos. Além disso, apenas uma única página de ASP.NET usa o CL (~/Caching/FromTheArchitecture.aspx
) todas as outras ainda fazem referência direta à BLL. Se você planeja usar uma CL em seu aplicativo, todas as chamadas da Camada de Apresentação devem ir para a CL, o que exigiria que as classes e métodos da CL cobrissem essas classes e métodos na BLL usada atualmente pela Camada de Apresentação.
Resumo
Embora o cache possa ser aplicado na camada de apresentação com controles SqlDataSource e ObjectDataSource de ASP.NET 2.0 s, idealmente as responsabilidades de cache seriam delegadas a uma camada separada na arquitetura. Neste tutorial, criamos uma camada de cache que reside entre a camada de apresentação e a camada de lógica de negócios. A camada de cache precisa fornecer o mesmo conjunto de classes e métodos que existem na BLL e são chamados a partir da camada de apresentação.
Os exemplos de camada de cache que exploramos neste e nos tutoriais anteriores exibiram carregamento reativo. Com o carregamento reativo, os dados são carregados no cache somente quando uma solicitação para os dados é feita e esses dados estão ausentes do cache. Os dados também podem ser carregados proativamente no cache, uma técnica que carrega os dados no cache antes que eles sejam realmente necessários. No próximo tutorial, veremos um exemplo de carregamento proativo quando analisarmos como armazenar valores estáticos no cache na inicialização do aplicativo.
Feliz Programação!
Sobre o Autor
Scott Mitchell, autor de sete livros sobre ASP/ASP.NET e fundador da 4GuysFromRolla.com, trabalha com tecnologias Web da Microsoft desde 1998. Scott trabalha como consultor, formador e escritor independente. Seu último livro é Sams Teach Yourself ASP.NET 2.0 in 24 Hours. Ele pode ser contatado em mitchell@4GuysFromRolla.com.
Um agradecimento especial a
Esta série de tutoriais foi revisada por muitos revisores úteis. A revisor principal deste tutorial foi Teresa Murphy. Interessado em rever meus próximos artigos do MSDN? Se for o caso, envie-me uma mensagem para mitchell@4GuysFromRolla.com.