Nel post di ieri ho detto che mi piace molto utilizzare le enumerazioni (System.Enum) nei miei ViewModel, le enumerazioni sono un gran comode in quanto fortemente tipizzate, giustamente Matteo però nei commenti chiede:

Domanda: se tutte le proprietà ed i dati devono essere localizzabili gli enum sono ancora cosi necessari?

In generale la risposta è si, vediamo nel dettaglio cosa faccio. Ecco come definisco una enumerazione:

public enum AdvertisementInvoiceCadence
{
    None = 0,
    
    [EnumItemDescription( "Immediata", "La fattura verrà emessa immediatamente e contestualmente alla creazione del contratto.", 1 )]
    AgreementContextual = 1,
    
    [EnumItemDescription( "In data di pubblicazione", "La fattura verrà emessa dopo la pubblicazione dell'inserzione con data uguale a quella di pubblicazione.", 2 )]
    PublicationContextual = 2,

    [EnumItemDescription( "Mensile", "La fatturazione avrà cadenza mensile, ogni mese verrà emessa una fattura per le inserzioni pubblicate nel mese.", 3 )]
    Monthly = 3,

    [EnumItemDescription( "Bimestrale", "La fatturazione avrà cadenza bimestrale, ogni 2 mesi verrà emessa una fattura per le inserzioni pubblicate nei 2 mesi precedenti.", 4 )]
    Bimonthly = 4,

    [EnumItemDescription( "Trimestrale", "La fatturazione avrà cadenza trimestrale, ogni 3 mesi verrà emessa una fattura per le inserzioni pubblicate nei 3 mesi precedenti.", 5 )]
    Quarterly = 5,

    [EnumItemDescription( "Semestrale", "La fatturazione avrà cadenza semestrale, ogni 6 mesi verrà emessa una fattura per le inserzioni pubblicate nei 6 mesi precedenti.", 6 )]
    HalfYearly = 6
}

quel EnumItemDescriptionAttribute è un normalissimo attributo:

[AttributeUsage( AttributeTargets.Field, AllowMultiple = false, Inherited = false )]
public class EnumItemDescriptionAttribute : Attribute
{
    readonly String _caption;
    readonly String _description;
    readonly Int32 _index;

    public EnumItemDescriptionAttribute( String caption )
        : this( caption, String.Empty, -1 )
    {

    }

    public EnumItemDescriptionAttribute( String caption, Int32 index )
        : this( caption, String.Empty, index )
    {

    }

    public EnumItemDescriptionAttribute( String caption, String description, Int32 index )
    {
        if( caption == null )
        {
            throw new ArgumentNullException( "caption" );
        }

        if( description == null )
        {
            throw new ArgumentNullException( "description" );
        }

        this._caption = caption;
        this._description = description;
        this._index = index;
    }

    public String Caption
    {
        get { return this.OnGetCaption(); }
    }

    public String Description
    {
        get { return this.OnGetDescription(); }
    }

    public virtual Int32 Index
    {
        get { return this._index; }
    }

    protected virtual String OnGetCaption()
    {
        return this._caption;
    }

    protected virtual String OnGetDescription()
    {
        return this._description;
    }
}

Ogni commento è superfluo, solo i due metodi protected virtual OnGetXxxx() sono interessanti perchè servono per gestire la localizzazione, ma non è questo il problema adesso.

Come utilizziamo il tutto? facciamo un esempio di un ViewModel:

public interface IAgreementConversionWizardViewModel : …
{
    IEnumerable<EnumBinder<AdvertisementInvoiceCadence>> KnownAdvertisementInvoiceCadence { get; }
    AdvertisementInvoiceCadence SelectedAdvertisementInvoiceCadence { get; set; }
… }

Il ViewModel espone una “lista” di valori, un po’ starni ma siate fiduciosi, e una proprietà che rappresenta il valore selezionato, a runtime quello che succede è questo:

image

prodotto da questo xaml, ma la stessa cosa funziona con Windows Forms e Asp.Net:

<GroupBox Header="Cadenza">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <ComboBox ItemsSource="{Binding Path=KnownAdvertisementInvoiceCadence}" 
                  DisplayMemberPath="Caption" SelectedValuePath="Value" 
                  SelectedValue="{Binding Path=SelectedAdvertisementInvoiceCadence}" />

        <ScrollViewer Grid.Row="2" Margin="4" 
                  VerticalScrollBarVisibility="Auto" 
                  HorizontalScrollBarVisibility="Disabled">
            <TextBlock Text="{Binding Path=SelectedAdvertisementInvoiceCadence, Converter={StaticResource enumDescConverter}}"
           TextWrapping="Wrap" />
        </ScrollViewer>
    </Grid>
</GroupBox>

Il “barbatrucco” è fatto da “EnumBinder” e soci e si limita a questo:

this.KnownAdvertisementInvoiceCadence = EnumHelper.ExtractBindingData<AdvertisementInvoiceCadence>();

ExtractBindingData<T>() altro non fa che andare ad indagare il tipo enumerativo e costruire una IEnumerable<EnumBinder> con i dati definiti negli attributi:

public static IEnumerable<EnumBinder<T>> ExtractBindingData<T>()
{
    return ExtractBindingData<T>( null );
}

public static IEnumerable<EnumBinder<T>> ExtractBindingData<T>( Predicate<T> filter )
{
    var lst = new List<EnumBinder<T>>();

    Array objs = Enum.GetValues( typeof( T ) );
    foreach( Object obj in objs )
    {
        if( filter == null || filter( ( T )obj ) )
        {
            Enum e = ( Enum )obj;
            EnumItemDescriptionAttribute attribute;
            if( e.TryGetDescriptionAttribute( out attribute ) )
            {
                var eb = new EnumBinder<T>( attribute, ( T )obj );
                lst.Add( eb );
            }
        }
    }

    lst.Sort( ( v1, v2 ) => v1.Index.CompareTo( v2.Index ) );

    return lst.AsReadOnly();
}

Si potrebbe ipotizzare di creare una MarkupExtension che faccia il giochetto senza doverlo fare a mano nel ViewModel, non è che una riga di codice sia poi sto gran problema.

Quale è il vantaggio di questa soluzione?

  • Lato UI ho dei valori umanamente comprensibili;
  • Lato “codice” ho dei valori “macchinalmente” :-) comprensibili e garantiti da san Csc.exe!;
  • Gestisco tutto in un posto solo semplificando notevolmente la manutenzione perchè non devo ricordarmi di tenere allineati i 2 mondi;
  • Esiste infine un LocalizableEnumItemDescriptionAttribute che ha proprio il supporto per la localizzazione e permette di “esternalizzare” le stringhe che nell’esempio sono cablate nel codice;

.m