2014-05-15 13 views
5

Ho sviluppato un helper MVC per generare display e tabelle modificabili (è necessario un plug-in jquery per consentire l'aggiunta e l'eliminazione dinamiche di righe con postback completo nelle tabelle modificabili), ad es.Ottieni tipi utilizzando IEnumerable GetGenericArguments

@Htm.TableDisplayFor(m => m.MyCollection as ICollection) 

che utilizzato in combinazione con gli attributi includerà totali nel piè, aggiungere colonne per visualizzare e modificare collegamenti, render collegamenti ipertestuali per tipo complesso ecc esempio

[TableColumn(IncludeTotals = true)] 

Sto per pubblicarlo su CodeProject ma prima di farlo vorrei risolvere un problema. L'helper prima ottiene il ModelMetadata dall'espressione, verifica che implementa ICollection, poi si fa il tipo della collezione (si noti il ​​seguente frammento è da Accettate le risposte su così, ma come spiegato di seguito, non è del tutto corretto)

if (collection.GetType().IsGenericType) 
{ 
    Type type = collection.GetType().GetGenericArguments()[0] 

Il tipo viene utilizzato per generare ModelMetadata per l'intestazione della tabella (potrebbero non esserci righe nella tabella) e ogni riga nel corpo della tabella (nel caso in cui alcuni elementi siano tipi ereditati che hanno proprietà aggiuntive e altrimenti rovinerebbero il layout colonna)

foreach (var item in collection) 
{ 
    ModelMetadata itemMetadata = ModelMetadataProviders.Current 
    .GetMetadataForType(() => item, type); 

Quello che vorrei essere in grado di fare è utilizzare IEnumerable anziché ICollection in modo che non sia necessario chiamare .ToList() sulle espressioni linq.

Nella maggior parte dei casi, IEnumerable funziona correttamente, ad es.

IEnumerable items = MyCollection.Where(i => i....); 

è OK perché .GetGenericArguments() restituisce una matrice contenente un solo tipo. Il problema è che '.GetGenericArguments()' su alcune query restituisce 2 o più tipi e non sembra esserci alcun ordine logico. Ad esempio

IEnumerable items = MyCollection.OrderBy(i => i...); 

restituisce [0] il tipo nella raccolta e [1] il tipo utilizzato per l'ordine.

In questo caso .GetGenericArguments()[0] funziona ancora, ma

MyCollection.Select(i => new AnotherItem() 
{ 
    ID = i.ID, 
    Name = 1.Name 
} 

ritorna [0] il tipo della collezione originale e [1] il tipo di AnotherItem

Quindi .GetGenericArguments()[1] è quello che mi serve per rendere il tabella per AnotherItem.

La mia domanda è: esiste un modo affidabile che utilizza le istruzioni condizionali per ottenere il tipo di cui ho bisogno per eseguire il rendering della tabella?

Dai miei test fino ad ora, utilizzando .GetGenericArguments().Last() funziona in tutti i casi tranne quando si utilizza OrderBy() perché la chiave di ordinamento è l'ultimo tipo.

Un paio di cose che ho provato finora includere ignorando tipi che sono tipi di valore (come spesso il caso con OrderBy(), ma OrderBy() query potrebbe utilizzare un string (che potrebbe essere verificata) o, peggio ancora, una classe che overload ==, < e> operatori (nel qual caso non sarei in grado di dire quale sia il tipo corretto), e non sono stato in grado di trovare un modo per verificare se la collezione implementa IOrderedEnumerable.

+0

S o, se capisco, tu feed _some_ 'IEnumerable ' questo è il risultato (in genere) di qualche query _arbitrary_ LINQ. E vuoi ottenere in modo affidabile la parte 'T' di' IEnumerable '? –

+0

@Chris Sì, anche se potrebbe essere solo IEnumerable (ad es. ArrayList) –

+1

In tal caso, provare iterando sulle interfacce implementate, trovare l'interfaccia 'IEnumerable ' ed estrarre la parte' T': 'var type = items.GetType() .GetInterfaces(). Where (t => t.IsGenericType) .Where (t => t.GetGenericTypeDefinition() == typeof (IEnumerable <>)). Single(). GetGenericArguments() [0]; 'Nota il mio uso di 'Single': se l'interfaccia non è implementata fallisce, o se implementa due (o più) diverse interfacce' IEnumerable ', allora fallisce. Puoi cambiarlo per usare 'FirstOrDefault' o' First' e gestirlo come preferisci per quei casi extra. –

risposta

4

Risolto (usando commenti pubblicati da Chris Sinclair)

private static Type GetCollectionType(IEnumerable collection) 
{ 
    Type type = collection.GetType(); 
    if (type.IsGenericType) 
    { 
    Type[] types = type.GetGenericArguments(); 
    if (types.Length == 1) 
    { 
     return types[0]; 
    } 
    else 
    { 
     // Could be null if implements two IEnumerable 
     return type.GetInterfaces().Where(t => t.IsGenericType) 
     .Where(t => t.GetGenericTypeDefinition() == typeof(IEnumerable<>)) 
     .SingleOrDefault().GetGenericArguments()[0]; 
    } 
    } 
    else if (collection.GetType().IsArray) 
    { 
    return type.GetElementType(); 
    } 
    // TODO: Who knows, but its probably not suitable to render in a table 
    return null; 
} 
+0

Per quanto riguarda l'utilizzo di 'SingleOrDefault' e il commento che" Potrebbe essere nullo se implementa due IEnumerable "; questo non è vero 'SingleOrDefault' creerà un'eccezione_ se ce n'è più di una. Anche se restituisse 'null' (cosa che succederà se non implementa le interfacce' IEnumerable '), otterrete immediatamente un' NullReferenceException' quando tentate di chiamare 'GetGenericArguments'. Ti consiglio di suddividere la query ed eseguire controlli più accurati. –

+0

Inoltre, preferirei personalmente eseguire prima il controllo dell'interfaccia anziché eseguire il default del primo parametro di tipo generico. Anche se improbabile, è ancora possibile avere una definizione di classe come 'classe MyStuff : IEnumerable '; molto probabilmente in questo caso, specialmente se ti _iterate_ su di essi in seguito (usando 'foreach' o chiamando' GetEnumerator') vorresti favorire il tipo 'int' (poiché sarà quello che viene iterato) piuttosto che il' Foo' genere. –

+0

@Chris Sì, è giusto - un errore stupido - grazie. –

Problemi correlati