Premessa

Un altro problema con il quale mi sono scontrato in questi giorni era la possibilità di avere una collection di elementi per una mia custom section, ma qualcosa che fosse relativamente semplice, e basato su elementi personalizzati. Insomma l'esatto contrario dell'esempio fornito con MSDN, che - IMHO - non era così immediato come speravo, fors'anche perchè è stato concepito per più usi ben più complessi rispetto alla mera collection di sola lettura alla quale miravo io.

La necessità

Volevo ottenere una collection di custom elements all'interno di un file di configurazione, in modo da avere più elementi dello stesso tipo.

La soluzione

Di default, quando si crea una ConfigurationElementCollection, di base la mappatura utilizzata con la proprietà ConfigurationElementCollectionType è di tipo AddRemoveClearMap, il che significa poter interagire con gli elementi che appartengono alla collection esclusivamente con i rispettivi tag Add, Remove e Clear (guarda caso il nome).
Visto che a me non piaceva proprio quell'Add, dovevo trovare un sistema che mi desse un pò più di libertà, e guarda caso il BasicMap lascia tutta la libertà di cui hai bisogno, ivi compresa quella di gestire la ConfigurationProperty con la quale specificare il nome della proprietà che deve corrispondere al nome dell'elemento presente all'interno del file di configurazione.

Ad essere onesto ci sono ancora diverse cosette che non ho compreso del meccanismo delle collection riferito a questo contesto. Per esempio alcuni esperimenti che ho fatto cercando di far partire il tutto dalla classe collection da utilizzare immediatamente in un foreach ... next anzichè da quella section, oppure quello di aggiungere una proprietà della Section che mi restituisse un oggetto corrente (con relative proprietà MoveNext() e Reset(), ma sfortunatamente 3 volte 4 il debug mi va in errore restituendomi di fatto un oggetto collection vuoto, quando di fatto se nascondo quella proprietà dal codice e metto un breakpoint in coda all'ultima graffa, la collection count la vedo piena e perfettamente accessibile. Ma vabbè, che volete, sono ancora agli inizi e sbagliando s'impara. Se imparerò come risolvere la situazione, posterò la situazione; altrimenti come dicono a casa mia "ciccia" o "amen" o quella che preferite).

Comunque, vediamo di capire quello che ho capito io da tutto il discorso. Inizio con un estratto del file web.config e la classe che gestisce gli elements che non starò a descrivere analiticamente poichè, benchè facciano parte dell'esempio, sono in questo preciso istante fuorvianti dal concetto collection (e comunque estremamente semplici da comprendere):

<configSections>
    <section name="MyUrls" 
           type="Andrea.ConfigElementCollection.UrlsSection" />
</configSections>
<MyUrls>
    <urls>
        <address name="Microsoft" 
                 url="http://www.microsoft.com" 
                 port="0"/>
                 
        <address name="Contoso" 
                 url="http://www.contoso.com/" 
                 port="8080"/>
    </urls>
</MyUrls>

// Define the UrlConfigElement.
public class UrlConfigElement :
        ConfigurationElement
{
    
public UrlConfigElement(){}

    
public UrlConfigElement(String newName, String newUrl,
          Int16 newPort)
    {
        Name = newName;
        Url = newUrl;
        Port = newPort;
    }

    
public UrlConfigElement(String elementName)
    {
        Name = elementName;
    }

    [ConfigurationProperty("name",
            DefaultValue = "Microsoft",
            IsRequired = 
true,
            IsKey = 
true)]
    
public string Name
    {
        
get
        
{
            
return (string)this["name"];
        }
        
set
        
{
            
this["name"] = value;
        }
    }

    [ConfigurationProperty("url",
            DefaultValue = "http://www.microsoft.com",
            IsRequired = 
true)]
    [RegexStringValidator(@"\w+:\/\/[\w.]+\S*")]
    
public string Url
    {
        
get
        
{
            
return (string)this["url"];
        }
        
set
        
{
            
this["url"] = value;
        }
    }

    [ConfigurationProperty("port",
            DefaultValue = (
int)0,
            IsRequired = 
false)]
    [IntegerValidator(MinValue = 0,
            MaxValue = 8080, ExcludeRange = 
false)]
    
public int Port
    {
        
get
        
{
            
return (int)this["port"];
        }
        
set
        
{
            
this["port"] = value;
        }
    }
}

Quindi, un frammento di codice della pagina default.aspx con la quale ho fatto i tests.

Andrea.ConfigElementCollection.UrlsSection config = 
 (Andrea.ConfigElementCollection.UrlsSection)
  WebConfigurationManager.GetSection("MyUrls");

Debug.WriteLine(config.urls["Microsoft"]);

Quando si chiama la GetSection, specificando il nome della sezione che voglio andare a recuperare, la classe WebConfigurationManager, internamente sà già quale è il file di configurazion della mia applicazione e con la specifica chiamata non fa altro che andare a recuperare in una variabile interna tutto il contenuto della section indicata - in questo caso - <MyUrls> e </MyUrls> - e e passi ogni singolo tag che incontra alla classe UrlsSection che cercherà al suo interno un riscontro per ogni tag incontrato.

Ne consegue che il primo elemento che trova è <urls>, quindi ... bisogna che all'interno della classe Section ci sia una proprietà denominata allo stesso identico modo.
Attenzione, quello che è importante non è il nome della proprietà di per se, che può essere quello che vi pare (anche se per questioni pratiche è meglio che coincida - magari ci sarà pure una nota MS su come si programma e sul fatto che debbano invece essere uguali, ma se c'è non l'ho trovata - ancora) ma il nome che si scrive dentro ai marcatori [ConfigurationProperty("urls")].

Sicchè, nella classe Section avremo questo frammento di codice:

[ConfigurationProperty("urls", IsDefaultCollection = true)]
public UrlsCollection urls
{
    
get
    
{
        
return (UrlsCollection)base["urls"];
    }
}

Al suo interno, alla proprietà gli si chiede di ritornare un oggetto di tipo collection specificandogli che gli elementi che ne dovranno far parte sono quelli all'interno dei marcatori <urls>. Come vedete fino a qui, la nostra collection non sà ancora quali elementi saranno al suo interno.

Ma andando oltre. Il debug - visto che vi sto raccontando tutto step-by-step (o almeno ci provo), a questo punto passa nella classe UrlsCollection, per il costruttore di default, quindi immediatamente alla proprietà CollectionType per sapere di che tipo è la collezione (vedi la nota all'inizio) e poi, scoperto che è una collection di tipo BasicMap, visto che presente, andrà nella proprietà ElementName dove è specificato il tag di apertura per gli elementi presenti in <urls>, in questo caso <address ...> .

Tecnicamente tutto questo processo dovrebbe avvenire in sorta di contemporaneità tra le operazioni di lettura e confronto, il fatto di passare per ElementName è una logica conseguenza per il compilatore che trovandosi sulla prima riga <address ...>, essendo un elemento nuovo per la Collection, non potrà far altro che passare per il metodo CreateNewElement con il quale verrà generata la nuova istanza di UrlElementCollection (leggendo quindi tutti i tag che fanno parte di quell'elemento address, verificando che gli attributi corrispondano alle effettive proprietà dalla classe, che i valori siano validabili e validati, ecc. ecc.) che sarà inserita nella collezione.
Ecco il blocco di codice della classe UrlCollection appena descritto:

// UrlCollection class fragment
protected override String ElementName
{
  
getreturn "address"; }
}
      
public override ConfigurationElementCollectionType CollectionType
{
  
get
  
{
    
return ConfigurationElementCollectionType.BasicMap;
  }
}

protected override ConfigurationElement CreateNewElement()
{
  
return new UrlConfigElement();
}

Una volta creato il nuovo elemento, il codice ritorna di nuovo alla UrlsCollection e passa per la proprietà GetElementKey (che assieme alla CreateNewElement sono gli unici due metodi "must implement" della ConfigurationElementCollection) dove non si fa altro che specificare quale è la chiave dell'oggetto element che - di fatto essendo una chiave è quella che tecnicamente dovrebbe essere univoca e comunque - fornisce l'accesso diretto tramite l'implementazione dell'indexer ai singoli Element della collection (infatti il tipo restituito, molto genericamente, è un object).

protected override Object 
        GetElementKey(ConfigurationElement element)
{
  
return ((UrlConfigElement)element).Name;
}

Tutto questo processo poc'anzi descritto, viene eseguito tante volte quante sono in realtà gli elementi presenti nella nostra sezione personalizzata del web.config.
Testando con uno step-by-step, ho potuto comunque notare che il debug ha modo di passare per la proprietà ConfigurationElementCollectionType più volte di quelle che in realtà sono gli elementi, e per questo onestamente non ho ancora una risposta.

Comunque, l'ultima volta che il compilatore passa per la GetElementKey è anche quella dove di fatto la classe ha finito il suo compito, e può quindi accingersi a restituire il nostro oggetto Section, memorizzato nel mio caso nella variabile config. Da questo momento in poi, con le altre proprietà e metodi della classe si può "navigare" tra gli elementi, richiedendo una IndexOf di un preciso elemento, o un preciso elemento partendo dalla sua chiave.

Ma torniamo, prima di concludere, all'ultima riga del codice della pagina aspx, quella con la Debug.WriteLine(config.urls["Microsoft"]); il nostro oggetto config essendo ora popolato con i dati presenti nel file di configurazione, mi consente di accedere tramite un indexer a disposizione ad un singolo elemento della collezione. In questo caso gli indexer sono due, uno per indice numerico e l'altro per chiave di tipo stringa.
L'indexer, al suo interno richiamando la BaseGet (che non fa altro che fare un ciclo tra gli elementi della collezione recuperata) confronterà il valore passato con la proprietà del ConfigurationElement il cui attributo IsKey è stato impostato su true. In caso positivo restituirà l'oggetto ConfigurationElement, diversamente un oggetto di tipo null.

Concludo con le due classi UrlsCollection e UrlsSection complete:

//Define the UrlsCollection that contains 
// UrlsConfigElement elements.
public class UrlsCollection : ConfigurationElementCollection
{
  
public UrlsCollection()
  {
  }
    
  
protected override String ElementName
  {
    
getreturn "address"; }
  }
      
  
public override ConfigurationElementCollectionType CollectionType
  {
    
get
    
{
      
return ConfigurationElementCollectionType.BasicMap;
    }
  }

  
protected override ConfigurationElement CreateNewElement()
  {
    
return new UrlConfigElement();
  }

  
protected override Object GetElementKey
            (ConfigurationElement element)
  {
    
return ((UrlConfigElement)element).Name;
  }
      
  
public new int Count
  {
    
get return base.Count; }
  }

  
public UrlConfigElement this[int index]
  {
    
get
    
{
      
if (index >= base.Count)
        
return null;
                    
      
return (UrlConfigElement)BaseGet(index);
    }
  }

  
new public UrlConfigElement this[string Name]
  {
    
get
    
{
      
return (UrlConfigElement)BaseGet(Name);
    }
  }

  
public int IndexOf(UrlConfigElement url)
  {
    
return BaseIndexOf(url);
  }
}

// Define a custom section containing 
// a simple element and a collection of 
// the same element. It uses two custom 
// types: UrlsCollection and 
// UrlsConfigElement.
public class UrlsSection : ConfigurationSection
{
  
public UrlsSection() 
  {
  }

  [ConfigurationProperty("urls", IsDefaultCollection = 
true)]
  
public UrlsCollection urls
  {
    
get
    
{
      
return (UrlsCollection)base["urls"];
    }
  }
}

Technorati Tags: , , ,