MSDN Tips & Tricks
I consigli degli esperti italiani per sfruttare al meglio gli strumenti di sviluppo e semplificare l’attività quotidiana.
In questa pagina
Pattern idiomatico per la definizione di collezioni tipizzate nelle API pubbliche
L’interfaccia INotifyPropertyChanged
Serializzazione di oggetti mediante un Serialization Surrogate
Calcolare la dimensione di un record
Effettuiamo una telefonata con C# e Windows Mobile 5.0
Inseriamo un appuntamento in Pocket Outlook via C#
Master Page annidate con ASP.NET 2.0
Pattern idiomatico per la definizione di collezioni tipizzate nelle API pubbliche
Di Riccardo Golia - Microsoft MVP
I tipi generici (generics) rappresentano senza dubbio una delle novità più interessanti e innovative tra quelle introdotte con la versione 2.0 di .NET Framework, in particolar modo, se si considera la definizione di tipi che rappresentano insiemi di dati omogenei.
La possibilità di definire collezioni tipizzate dove il tipo degli elementi costitutivi è parametrico introduce una serie di vantaggi senza precedenti in un'ottica di riusabilità. L'uso del parametro di tipo nella definizione delle collezioni tipizzate permette inoltre di evitare a seconda dei casi le costose operazioni di boxing/unboxing e upcasting/downcasting legate all'uso di System.Object, con guadagni in termini di performance davvero considerevoli.
I namespace System.Collections.Generic e System.Collection.ObjectModel contengono le collezioni generiche direttamente incluse in .NET Framework. Se i tipi inclusi nel primo namespace hanno una valenza generale, quelli inclusi nel secondo e, in particolare, Collection<T> (in Visual Basic Collection(Of T)) sono stati specificatamente pensati per essere utilizzati nell'ambito della modellazione dei domini applicativi e per rappresentare liste di entità e oggetti del dominio.
Le classi incluse in System.Collection.ObjectModel sono da preferire ai tipi generici presenti in System.Collections.Generic, come per esempio List<T> (in Visual Basic List(Of T)), quando l'esigenza sia quella di definire una collezione tipizzata in un'API pubblica. Le motivazioni sono principalmente due.
- List<T> non è estendibile dal momento che non è possibile fare l'overriding dei suoi membri. Nessuno dei suoi metodi è virtuale, pertanto il tipo non si presta a essere esteso sfruttando il polimorfismo. Viceversa, i metodi protetti SetItem, InsertItem, RemoveItem e ClearItems di Collection<T> sono virtuali ed estendibili a piacere nelle classi derivate.
- L'interfaccia di List<T> è troppo ricca, dal momento che presenta un numero elevato di membri, alcuni dei quali inutili nella maggior parte degli scenari. L'interfaccia di Collection<T> è più snella e si presta meglio a essere usata per rappresentare collezioni di oggetti del dominio. Di fatto Collection<T> funge da wrapper per il tipo List<T>, del quale alleggerisce l'interfaccia per rendere la classe risultante più adattabile alle esigenze di modellazione.
Un'ulteriore considerazione riguarda la scelta del tipo astratto da associare alle collezioni di oggetti nelle API pubbliche. L'interfaccia da utilizzare in generale è IList<T> (in Visual Basic IList(Of T)), che include e definisce i comportamenti classici delle collezioni di dati.
L'esempio di codice riportato di seguito mostra un semplice caso di utilizzo di Collection<T>: l'interfaccia della classe PersonCollection che deriva da Collection<Person> viene estesa e arricchita unicamente con membri validi per gli scenari nei quali la collezione viene usata.
[C#]
using System.Collections.Generic; using System.Collections.ObjectModel; namespace Microsoft.MsdnItaly.Tips { public class Person { private string _name; private int _age; public string Name { get { return _name; } set { _name = value; } } public int Age { get { return _age; } set { _age = value; } } } public class PersonCollection : Collection<Person> { public PersonCollection() : base() { } public PersonCollection(IList<Person> list) : base(list) { } protected override void InsertItem(int index, Person item) { if((!string.IsNullOrEmpty(item.Name)) && (item.Age >= 0)) base.InsertItem(index, item); } protected override void SetItem(int index, Person item) { if ((!string.IsNullOrEmpty(item.Name)) && (item.Age >= 0)) base.SetItem(index, item); } public PersonCollection FindByAge() { List<Person> items = base.Items as List<Person>; IList<Person> list = items.FindAll( delegate(Person item) { return item.Age >= 18; }); return new PersonCollection(list); } } }
[Visual Basic]
Imports System.Collections.Generic Imports System.Collections.ObjectModel Namespace Microsoft.MsdnItaly.Tips Public Class Person Private _name As String Private _age As Integer Public Property Name() As String Get Return _name End Get Set(ByVal value As String) _name = value End Set End Property Public Property Age() As Integer Get Return _age End Get Set(ByVal value As Integer) _age = value End Set End Property End Class Public Class PersonCollection Inherits Collection(Of Person) Public Sub New() MyBase.New() End Sub Public Sub New(ByVal list As IList(Of Person)) MyBase.New(list) End Sub Protected Overrides Sub InsertItem(ByVal index As Integer, ByVal item As Person) If ((Not String.IsNullOrEmpty(item.Name)) And (item.Age >= 0)) Then MyBase.InsertItem(index, item) End If End Sub Protected Overrides Sub SetItem(ByVal index As Integer, ByVal item As Person) If ((Not String.IsNullOrEmpty(item.Name)) And (item.Age >= 0)) Then MyBase.SetItem(index, item) End If End Sub Public Function FindByAge() As PersonCollection Dim items As List(Of Person) = DirectCast(MyBase.Items, List(Of Person)) Dim list As IList(Of Person) = items.FindAll(AddressOf EvaluateAge) Return New PersonCollection(list) End Function Private Function EvaluateAge(ByVal item As Person) As Boolean Return item.Age >= 18 End Function End Class End Namespace
L’interfaccia INotifyPropertyChanged
Di Mauro Servienti - Microsoft MVP
Il motore di Data Binding messo a disposizione da .NET Framework è monodirezionale, questo significa che le variazioni vengono gestite in un solo senso che per l’esattezza è verso lo strato di persistenza dei dati.
Cerchiamo di chiarire questo punto con un esempio: se abbiamo un’applicazione Windows Forms con una semplice TextBox in binding con una sorgente dati, come ad esempio un nostro oggetto custom, noteremo che le modifiche apportate al contenuto della TextBox vengono immediatamente persistite sul nostro oggetto, mentre le eventuali modifiche apportate da codice al nostro oggetto non vengono in nessun modo recepite dall’interfaccia utente. Se invece, in binding con la TextBox, ci fosse un’istanza di una DataView (nello specifico un’istanza di una DataRowView) il sistema diventa immediatamente bidirezionale.
Come avviene tutto ciò?
In .NET Framework 1.x il motore di binding, quando esegue per la prima volta il binding tra il controllo Windows Forms e la proprietà esposta dall’oggetto contenente i dati, cerca, via reflection, un evento che ha lo stesso nome della proprietà con il suffisso “Changed”, è quindi sufficiente avere una struttura del genere:
private String _myStringValue; public String MyStringValue { get { return _myStringValue; } set { if( value != this.MyStringValue ) { _myStringValue = value; //Invochiamo l’evento che notifica la variazione del valore this.OnMyStringValueChanged(); } } } public event EventHandler MyStringValueChanged = null; protected virtual void OnMyStringValueChanged() { if( this.MyStringValueChanged != null ) { this.MyStringValueChanged( this, EventArgs.Empty ); } }
Abbiamo quindi una normalissima proprietà di tipo System.String che si chiama MyStringValue e un evento che si chiama MyStringValueChanged, al momento del primo binding .NET Framework cerca eventuali eventi con questa specifica nomenclatura e li sottoscrive, allo scatenarsi dell’evento andrà a rileggere la proprietà e aggiornerà il valore presente sull’interfaccia utente.
Il codice appena esposto, nonostante svolga egregiamente il suo compito, soffre comunque di due problemi: il primo strutturale, siamo obbligati a riempire i nostri oggetti di eventi e codice che poco hanno a che spartire con la nostra logica di business e siamo obbligati a manutenere nel tempo tale codice, il secondo di performance, .NET Framework cerca gli eventi via reflection e questo ha sicuramente un impatto sulle performance generali della nostra applicazione.
Il sistema è stato rivoluzionato con .NET Framework 2.0 con l’introduzione dell’interfaccia INotifyPropertyChanged (namespace System.ComponentModel).
Innanzitutto vediamo la firma della nuova interfaccia:
public interface INotifyPropertyChanged { event PropertyChangedEventHandler PropertyChanged; } public delegate void PropertyChangedEventHandler( object sender, PropertyChangedEventArgs e ); public class PropertyChangedEventArgs : EventArgs { public PropertyChangedEventArgs( string propertyName ); public virtual string PropertyName { get; } }
Il motore di Data Binding, durante il primo binding, verifica se l’oggetto con a cui sta legando i controlli Windows Forms implementa l’interfaccia INotifyPropertyChanged, in caso affermativo non fa altro che sottoscrivere l’evento PropertyChanged che notificherà all’interfaccia utente che il valore di una delle proprietà dell’oggetto è cambiato facendo si che l’interfaccia utente aggiorni il contenuto del controllo Windows Forms corrispondente.
Il motore di Data Binding saprà quale controllo andare ad aggiornare e quale proprietà rileggere grazie all’informazione presente negli argomenti dell’evento che riportano il nome della proprietà che ha subito una variazione.
Anche in questo caso vediamo un esempio di implementazione:
private Int32 _myIntegerValue; public Int32 MyIntegerValue { get { return _myIntegerValue; } set { if( value != this.MyIntegerValue ) { _myIntegerValue = value; //Invochiamo l’evento che notifica la variazione del valore this.OnPropertyChanged( "MyIntegerValue" ); } } } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged( String propertyName ) { if( this.PropertyChanged != null ) { /* Prepariamo l’istanza degli EventArgs che verranno passati al subscriber dell’evento */ PropertyChangedEventArgs args = new PropertyChangedEventArgs( propertyName ); //Scateniamo l’evento this.PropertyChanged( this, args ); } }
Il codice risultante sarà sicuramente più pulito e lineare perchè conterrà un solo evento, che volendo potremmo anche nascondere implementando l’interfaccia in maniera esplicita, rendendo quindi invisibile l’evento all’intellisense.
Infine un’importante considerazione è che il motore di Data Binding non ha più la necessità di analizzare via reflection i nostri oggetti alla ricerca di eventi con una nomenclatura specifica, guadagnando sicuramente in performance.
Risorse:
Serializzazione di oggetti mediante un Serialization Surrogate
Di Corrado Cavalli - Microsoft MVP
Oggi non è il vostro giorno fortunato: dovete serializzare un tipo Person sealed (NotInheritable in VB) che sebbene sia stato decorato con l’attributo Serializable espone un tipo Address che non lo è.
La soluzione consiste nel delegare a un Surrogate Selector la serializzazione del tipo Person.
Supponiamo che quello che segue sia il codice delle classi Person e Address:
C#
using System.Runtime.Serialization.Formatters.Binary; using System.IO; using System.Runtime.Serialization; using System.Reflection; [Serializable()] public class Person { private string fullName; private Address address=new Address(); public string FullName { get { return fullName; } set { fullName = value; } } public Address Address { get { return address; } } } public class Address { private string streetName; public string StreetName { get { return streetName; } set { streetName = value; } } } Person p=new Person(); p.FullName = "Mario Rossi"; p.Address.StreetName = "Via Roma 10"; using (Stream stream = new FileStream("data.bin", FileMode.Create, FileAccess.Write, FileShare.None)) { BinaryFormatter bf = new BinaryFormatter(); bf.Serialize(stream, p); }
VB
Imports System.Runtime.Serialization.Formatters.Binary Imports System.IO Imports System.Runtime.Serialization Imports System.Reflection <Serializable()> _ Public Class Person Private _fullName As String Private _address As New Address Public Property FullName() As String Get Return _fullName End Get Set(ByVal value As String) _fullName = value End Set End Property Public Property Address() As Address Get Return _address End Get Set(ByVal value As Address) _address = value End Set End Property End Class Public Class Address Private _streetName As String Public Property StreetName() As String Get Return _streetName End Get Set(ByVal value As String) _streetName = value End Set End Property End Class Dim p As New Person() p.FullName = "Mario Rossi" p.Address.StreetName = "Via Roma 10" Using stream As Stream = New FileStream("data.bin", FileMode.Create, FileAccess.Write, FileShare.None) Dim bf As New BinaryFormatter() bf.Serialize(stream, p) End Using
Cercando di serializzare un istanza di Person usando il codice sopra indicato si otterrà un eccezione “Type Address is not marked as Serializable”.
Definiamo quindi un surrogate selector che gestirà la serializzazione di Person implementando ISerializationSurrogate
C#
public class PersonSerializationSurrogate : ISerializationSurrogate { public void GetObjectData(object obj, SerializationInfo info, StreamingContext context) { Person p = obj as Person; info.AddValue("FullName", p.FullName); info.AddValue("AddressStreetName", p.Address.StreetName); } public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector) { Person p = obj as Person; p.FullName = info.GetString("FullName"); //Accedo al field privato! (rischioso…) FieldInfo fi = typeof(Person).GetField("address", BindingFlags.NonPublic | BindingFlags.Instance); fi.SetValue(p, new Address()); p.Address.StreetName = info.GetString("AddressStreetName"); return p; } }
VB
Public Class PersonSerializationSurrogate: Implements ISerializationSurrogate Public Sub GetObjectData(ByVal obj As Object, ByVal info As System.Runtime.Serialization.SerializationInfo, ByVal context As System.Runtime.Serialization.StreamingContext) Implements System.Runtime.Serialization.ISerializationSurrogate.GetObjectData Dim p As Person = DirectCast(obj, Person) info.AddValue("FullName", p.FullName) info.AddValue("AddressStreetName", p.Address.StreetName) End Sub Public Function SetObjectData(ByVal obj As Object, ByVal info As System.Runtime.Serialization.SerializationInfo, ByVal context As System.Runtime.Serialization.StreamingContext, ByVal selector As System.Runtime.Serialization.ISurrogateSelector) As Object Implements System.Runtime.Serialization.ISerializationSurrogate.SetObjectData Dim p As Person = DirectCast(obj, Person) p.FullName = info.GetString("FullName") 'Accedo al field privato! (rischioso…) Dim fi As FieldInfo = GetType(Person).GetField("_address", BindingFlags.NonPublic Or BindingFlags.Instance) fi.SetValue(p, New Address()) p.Address.StreetName = info.GetString("AddressStreetName") Return p End Function End Class
Andiamo ora a informare il formatter della presenza di un serializzatore ad-hoc per il tipo Person, aggiungendo a un’istanza di SurrogateSelector il surrogate PersonSerializationSurrogate e associando il SurrogateSelector al formatter.
Il codice che serializza Person diverrà quindi:
C#
Person p=new Person(); p.FullName = "Mario Rossi"; p.Address.StreetName = "Via Roma 10"; SurrogateSelector ss = new SurrogateSelector(); ss.AddSurrogate(typeof(Person), new StreamingContext(StreamingContextStates.All), new PersonSerializationSurrogate()); using (Stream stream = new FileStream("data.bin", FileMode.Create, FileAccess.Write, FileShare.None)) { BinaryFormatter bf = new BinaryFormatter(); bf.SurrogateSelector = ss; bf.Serialize(stream, p); }
VB
Dim p As New Person() p.FullName = "Mario Rossi" p.Address.StreetName = "Via Roma 10" Dim ss As New SurrogateSelector() ss.AddSurrogate(GetType(Person), New StreamingContext(StreamingContextStates.All), _ New PersonSerializationSurrogate()) Using stream As Stream = New FileStream("data.bin", FileMode.Create, FileAccess.Write, FileShare.None) Dim bf As New BinaryFormatter() bf.SurrogateSelector = ss bf.Serialize(stream, p) End Using
Vanno fatte alcune precisazioni:
La serializzazione avviene dall’esterno quindi solo ciò che è visible all’esterno può essere serializzato.
Il tipo passato al metodo SetObjectData() è stato creato usando FormatterService.CreateUninitializedObject() il che significa che il costruttore non è stato invocato e quindi tutti i campi interni non sono stati inizializzati.
Una possibile soluzione a questa limitazione può essere quella di utilizzare Reflection e accedere comunque ai campi privati (come mostrato nell’esempio in SetObjectdata() per creare un’istanza valida di Address prima di valorizzarla) va comunque detto che questa è una tecnica rischiosa e applicabile solo in contesti di sicurezza fully-trusted.
La possibilità di indicare modalità di serializzazione diverse dello stesso tipo, a seconda del contesto di serializzazione utilizzato, fanno si che, malgrado le limitazioni indicate, l’utilizzo di un Surrogate può risolvere situazioni complesse.
Calcolare la dimensione di un record
Di Andrea Benedetti - Microsoft MVP
Stimare la quantità di spazio che sarà, presumibilmente, necessaria per memorizzare le informazioni all’interno di un database è sicuramente un’operazione utile in fase di definizione e modellazione della nostra base dati.
Per prima cosa per poter stimare in maniera ragionevole lo storage che sarà necessario, in secondo luogo per definire in fase di creazione del database una dimensione che consenta a SQL Server di non effettuare quelle operazioni di incremento automatico che, di fatto, per la loro invasività potrebbero rendere indisponibile, seppur per poco o pochissimo tempo, il nostro database (si pensi, ad esempio, a un incremento del 10% del nostro database quando lo stesso raggiunge una dimensione di qualche Gb).
Cerchiamo allora di capire come potremmo valutare in maniera comoda la stima dello spazio necessario, ovvero effettuare delle operazioni di SQL Sizing.
SQL Server, sappiamo, memorizza all’interno di tabelle di sistema tutti i metadati relativi alle tabelle definite all’interno del database.
Allo stesso modo sappiamo che, tramite le viste di sistema INFORMATION_SCHEMA, e, più precisamente la vista INFORMATION_SCHEMA.COLUMNS, possiamo recuperare informazioni sulle colonne che costituiscono le nostre tabelle, ovvero i loro tipi e le loro dimensioni.
Vediamo allora come potremmo, leggendo questa vista, utilizzando una serie di istruzioni CASE, valutando la proprietà NUMERIC_PRECISION delle colonne decimali e aggiungendo il numero di bytes che comunque sono necessari per la memorizzazione dei nostri dati, calcolare la dimensione massima che potrebbe raggiungere un record di una precisa tabella.
Nell’esempio andremo ad analizzare il database di esempio Adventure Works, liberamente installabile con il setup di SQL Server 2005.
USE AdventureWorks GO /* Imposto la tabella di cui voglio controllare la lunghezza del record */ declare @tabella sysname select @tabella = 'TransactionHistory' /* Trovo la lunghezza */ DECLARE @dimensione bigint SELECT @dimensione = 10 + SUM ( CASE DATA_TYPE WHEN 'bigint' THEN 8 WHEN 'int' THEN 4 WHEN 'smallint' THEN 2 WHEN 'tinyint' THEN 1 WHEN 'bit' THEN 1 WHEN 'datetime' THEN 8 WHEN 'smalldatetime' THEN 4 WHEN 'decimal' THEN CASE WHEN (NUMERIC_PRECISION Between 1 And 9) THEN 5 WHEN (NUMERIC_PRECISION Between 10 And 19) THEN 9 WHEN (NUMERIC_PRECISION Between 20 And 28) THEN 13 WHEN (NUMERIC_PRECISION Between 29 And 38) THEN 17 END WHEN 'numeric' THEN CASE WHEN (NUMERIC_PRECISION Between 1 And 9) THEN 5 WHEN (NUMERIC_PRECISION Between 10 And 19) THEN 9 WHEN (NUMERIC_PRECISION Between 20 And 28) THEN 13 WHEN (NUMERIC_PRECISION Between 29 And 38) THEN 17 END WHEN 'money' THEN 8 WHEN 'smallmoney' THEN 4 WHEN 'float' THEN 8 WHEN 'real' THEN 4 WHEN 'timestamp' THEN 8 WHEN 'image' THEN 16 WHEN 'text' THEN 16 WHEN 'ntext' THEN 16 WHEN 'uniqueidentifier' THEN 16 ELSE CASE WHEN DATA_TYPE like 'var%' THEN CHARACTER_MAXIMUM_LENGTH + 2 WHEN DATA_TYPE like 'nvar%' THEN CHARACTER_MAXIMUM_LENGTH + 2 ELSE CHARACTER_MAXIMUM_LENGTH END END ) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @tabella IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE DATA_TYPE Like '%var%' And TABLE_NAME = @tabella) SET @dimensione = @dimensione + 2 /* Visualizzo i risultati */ SELECT -- Nome della tabella @tabella as [Tabella], -- Dimesione calcolata fino ad ora @dimensione + ( -- Calcolo maschera di bit per valori NULL select 2 + (((SELECT count(column_Name) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @tabella) + 7) / 8) ) + -- Overhead intestazione di riga 4 AS [Dimensione] /* Quante righe stanno in una data page? */ declare @rowSize bigint set @rowSize = ( SELECT @dimensione + (select 2 + (((SELECT count(column_Name) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @tabella) + 7) / 8) ) + 4 ) -- Calcolo il numero di righe: select 8060 / (@rowSize + 2) as numeroRighePerDataPage
Un ragionamento particolare lo dovremmo inoltre fare sui nuovi tipi di dati varchar(max), nvarchar(max), varbinary(max) e xml.
Tali tipi di dati, che consentono di memorizzare fino a 2 Gb di informazioni, potrebbero essere memorizzati direttamente all’interno delle pagine dati (andando ad appesantire notevolmente le tabelle stesse) oppure all’esterno (consigliato), memorizzando all’interno nella data page soltanto un puntatore di 16 bytes (come avveniva per i campi blob delle precedenti versioni).
Per verificare le impostazioni di SQL Server è sufficiente verificare, tramite la procedura di sistema sp_tableoption l’opzione impostata, ad esempio: values types out of row.
Effettuiamo una telefonata con C# e Windows Mobile 5.0
Di Michele Locuratolo - Microsoft MVP
Windows Mobile 5.0 espone una serie di API che permettono di interagire con le funzionalità del Sistema Operativo direttamente dal mondo managed. Una di queste è la possibilità, per le versioni Phone Edition, di effettuare una telefonata direttamente da codice. Grazie al namespace Microsoft.WindowsMobile.Telephony sarà sufficiente scrivere le seguenti linee di codice:
private void btnChiama_Click(object sender, EventArgs e) { Phone myPhone = new Phone(); myPhone.Talk(“080123456789”, true); }
Il primo parametro rappresenta il numero telefonico da chiamare. Può essere ad esempio recuperato da una label o da una DataGrid. Il secondo parametro invece richiede conferma prima di comporre il numero. Grazie a queste poche righe di codice, ci sarà possibile effettuare una chiamata direttamente dalla nostra applicazione.
E’ consigliato il download di Windows Mobile 5.0 SDK per Pocket PC.
Inseriamo un appuntamento in Pocket Outlook via C#
Di Michele Locuratolo - Microsoft MVP
I dispositivi equipaggiati con Windows Mobile 5.0 sono dotati del comodissimo Pocket Outlook che, oltre a gestire le e-mail e la rubrica telefonica, ci permette di gestire gli appuntamenti. Inserire via codice un appuntamento direttamente in Pocket Outlook è una operazione davvero semplice. Supponiamo di voler inserire un appuntamento riguardante la consegna di un ordine selezionando la data da un controllo DateTimePicker:
Appointment appointment = new Appointment(); appointment.Subject = "Consegna ordine " + _CodiceOrdine; appointment.Start = new DateTime( dateTimePicker1.Value.Year, dateTimePicker1.Value.Month, dateTimePicker1.Value.Day, 10, 00, 00 ); appointment.Duration = new TimeSpan(00, 30, 00); appointment.ReminderVibrate = true; appointment.ReminderRepeat = true; using (OutlookSession session = new OutlookSession()){ session.Appointments.Items.Add(appointment); }
Tale codice inserirà nel calendario, alla data selezionata, un appuntamento impostandone l’oggetto, la durata ed il reminder.
E’ consigliato il download di Windows Mobile 5.0 SDK per Pocket PC.
Master Page annidate con ASP.NET 2.0
Di Daniele Bochicchio - Microsoft MVP
Le Master Page rappresentano una delle novità più ghiotte offerte dalla versione 2.0 di ASP.NET, poichè consentono di mantenere un layout uniforme tra tutte le pagine del sito senza fare grossi sforzi.
In un'applicazione web di certe dimensioni potrebbe essere comodo organizzare le Master Page in maniera tale che siano diversificate in base alla sezione, ma così che tutte facciano riferimento ad una struttura comune.
Si prenda ad esempio il caso in cui sia necessario definire una Master Page generica per tutto il sito e poi una specifica per l'ipotetica sezione "Articoli". La prima potrebbe essere semplicemente così:
<%@ Master Language="C#" CodeFile="MySite.master.cs" Inherits="MySite" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Sito</title> </head> <body> <form id="form1" runat="server"> <div> <asp:contentplaceholder id="Body" runat="server"> </asp:contentplaceholder> <hr /> <asp:contentplaceholder id="Footer" runat="server"> Footer generico </asp:contentplaceholder> </div> </form> </body> </html>
A questo punto, si può specificare un'altra Master Page che utilizzi quest'ultima, personalizzando il footer ed il corpo per la sezione articoli, in questo modo:
<%@ Master Language="C#" CodeFile="Articles.master.cs" Inherits="Articles" MasterPageFile="~/MySite.master" %> <asp:Content ID="ArticlesFooter" ContentPlaceHolderID="Footer" runat="server"> Footer degli articoli<br /> <asp:ContentPlaceHolder ID="Footer" runat="server"></asp:ContentPlaceHolder> </asp:Content> <asp:Content ID="ArticlesBody" ContentPlaceHolderID="Body" runat="server"> <p>Questo è il body degli articoli</p> <asp:ContentPlaceHolder ID="Body" runat="server"></asp:ContentPlaceHolder> </asp:Content>
Si noti l'uso dell'attributo MasterPageFile anche nella Master Page, che poi sarà usata dalla pagina, in questo modo:
<%@ Page Language="C#" CodeFile="Default.aspx.cs" Inherits="_Default" MasterPageFile="~/Articles.master" %> <asp:Content ContentPlaceHolderID="Footer" runat="server"> Articoli </asp:Content> <asp:Content ContentPlaceHolderID="Body" runat="server"> Questo è il corpo del sito. </asp:Content>
L'unico limite di questa tecnica è che Visual Studio 2005 non riesce a supportarli a design time, rendendolo meno comodo per chi è tradizionalmente abituato a sviluppare con questa modalità.