2015-06-05 12 views
18

ho seguito pseudo-codiceMantenere CurrentCulture in asincrone/attendere

string GetData() 
{ 
    var steps = new List<Task<string>> 
    { 
    DoSomeStep(), 
    DoSomeStep2() 
    }; 

    await Task.WhenAll(steps); 

    return SomeResourceManagerProxy.RetrieveValuesForLocalizedStrings(steps.Select(s => s.Result)); 

} 

Questo metodo viene chiamato da WebService, dove ho impostato Thread.CurrentUICulture in base alle impostazioni del browser dell'utente.

Dopo l'attesa, lo CurrentUICulture si perde (corro su un thread diverso).

ho risolto il problema con le seguenti:

public class MyAwaiter<T> : INotifyCompletion 
    { 
     private TaskAwaiter<T> waiter; 
     private CultureInfo culture; 

     public MyAwaiter(TaskAwaiter<T> waiter) 
     { 
      this.waiter = waiter; 
     } 

     public PreserveCultureAwaiter<T> GetAwaiter() { return this; } 

     public bool IsCompleted { get { return waiter.IsCompleted; } } 

     public void OnCompleted(Action continuation) 
     { 
      culture = Thread.CurrentThread.CurrentUICulture; 
      waiter.OnCompleted(continuation); 
     } 

     public T GetResult() 
     { 
      Thread.CurrentThread.CurrentUICulture = culture; 
      return waiter.GetResult(); 
     } 
    } 

    public static MyAwaiter<T> KeepCulture<T>(this Task<T> task) 
    { 
     return new MyAwaiter<T>(task.GetAwaiter()); 
    } 

... 

    await Task.WhenAll(steps).KeepCulture(); 

Questo ha uno svantaggio - necessità di ricordarsi di chiamare KeepCulture() su ogni operazione che viene atteso. (Ho anche qualche metodo di estensione per mantenere la cultura dell'interfaccia utente nell'attività).

C'è un modo più semplice per conservare la cultura dell'interfaccia utente?

+0

una nota: sto scrivendo framework per la localizzazione di cose che erano già state scritte in asincrono/attesa. Mi piacerebbe che tutto fosse il più trasparente possibile per gli altri sviluppatori – nothrow

+0

Ho visto una soluzione simile in uno dei repository .NET qualche tempo fa. Potrebbe essere stato EF. Penso che l'abbiano cancellato per qualche motivo, quindi è necessario eseguire la scansione dei log. – usr

+0

Forse il tempo di chiudere questo Q + A, il problema è stato risolto. –

risposta

19

La cultura non scorre in .NET Framework, un problema molto noto. È molto difficile da risolvere su Windows, la cultura è una unmanaged property di un thread, quindi il CLR non può garantire che sia sempre impostato correttamente. Questo rende armeggiare con CurrentCulture sul thread principale un grosso errore. Gli errori che ottieni sono molto difficili da diagnosticare. Come una SortedList, si crea su un thread che improvvisamente non viene più ordinato su un altro. Che schifo.

Microsoft ha fatto qualcosa a riguardo in .NET 4.5, hanno aggiunto la proprietà CultureInfo.DefaultThreadCurrentCulture. Anche DefaultThreadCurrentUICulture. Ciò non garantisce che sia impostato correttamente, il codice non gestito che chiami può cambiarlo e il CLR non può fare nulla al riguardo. In altre parole, un bug sarà molto più difficile da diagnosticare. Ma almeno hai l'idea diquando potrebbe cambiare.


UPDATE: questo problema è stato risolto completamente NET 4.6, cultura ora scorre da un filo all'altro e l'hack CultureInfo.DefaultThreadCurrentCulture non è più necessario né utile. Documentato nell'articolo MSDN per CultureInfo.CurrentCulture. I dettagli scritti in questo momento non sembrano essere del tutto corretti, fluivano sempre quando li ho testati e DefaultThreadCurrentCulture sembra non aver più alcun ruolo.

+0

Capisco il tuo consiglio, ma sfortunatamente non farlo è qualcosa che non posso fare in questo momento – nothrow

+0

Penso che questo dovrebbe essere contrassegnato come risposta. Per me questa sembra essere una soluzione adatta nel Framework 4.5. L'argomento secondo cui una chiamata non gestita può cambiare la cultura non è rilevante. L'impostazione predefinita può essere sovrascritta in una chiamata gestita. O non lo capisco profondamente (?) La domanda forse è perché un codice non gestito dovrebbe cambiare la cultura. Penso che normalmente tu definisca la cultura per tutti i thread nel tuo processo. –

3

Finora ho creato il mio SynchronizationContext, che ho testato con entrambe le applicazioni ASP.NET e console, e in entrambi mantiene la cultura come io lo voglio:

/// <summary> 
/// Class that captures current thread's culture, and is able to reapply it to different one 
/// </summary> 
internal sealed class ThreadCultureHolder 
{ 
    private readonly CultureInfo threadCulture; 
    private readonly CultureInfo threadUiCulture; 

    /// <summary> 
    /// Captures culture from currently running thread 
    /// </summary> 
    public ThreadCultureHolder() 
    { 
     threadCulture = Thread.CurrentThread.CurrentCulture; 
     threadUiCulture = Thread.CurrentThread.CurrentUICulture; 
    } 

    /// <summary> 
    /// Applies stored thread culture to current thread 
    /// </summary> 
    public void ApplyCulture() 
    { 
     Thread.CurrentThread.CurrentCulture = threadCulture; 
     Thread.CurrentThread.CurrentUICulture = threadUiCulture; 
    } 

    public override string ToString() 
    { 
     return string.Format("{0}, UI: {1}", threadCulture.Name, threadUiCulture.Name); 
    } 
} 

/// <summary> 
/// SynchronizationContext that passes around current thread's culture 
/// </summary> 
internal class CultureAwareSynchronizationContext : SynchronizationContext 
{ 
    private readonly ThreadCultureHolder cultureHolder; 
    private readonly SynchronizationContext synchronizationImplementation; 

    /// <summary> 
    /// Creates default SynchronizationContext, using current(previous) SynchronizationContext 
    /// and captures culture information from currently running thread 
    /// </summary> 
    public CultureAwareSynchronizationContext() 
     : this(Current) 
    {} 

    /// <summary> 
    /// Uses passed SynchronizationContext (or null, in that case creates new empty SynchronizationContext) 
    /// and captures culture information from currently running thread 
    /// </summary> 
    /// <param name="previous"></param> 
    public CultureAwareSynchronizationContext(SynchronizationContext previous) 
     : this(new ThreadCultureHolder(), previous) 
    { 
    } 

    internal CultureAwareSynchronizationContext(ThreadCultureHolder currentCultureHolder, SynchronizationContext currentSynchronizationContext) 
    { 
     cultureHolder = currentCultureHolder; 
     synchronizationImplementation = currentSynchronizationContext ?? new SynchronizationContext(); 
    } 

    public override void Send(SendOrPostCallback d, object state) 
    { 
     cultureHolder.ApplyCulture(); 
     synchronizationImplementation.Send(d, state); 
    } 

    public override void Post(SendOrPostCallback d, object state) 
    { 
     synchronizationImplementation.Post(passedState => 
     { 
      SetSynchronizationContext(this); 
      cultureHolder.ApplyCulture(); 
      d.Invoke(s); 
     }, state); 
    } 

    public override SynchronizationContext CreateCopy() 
    { 
     return new CultureAwareSynchronizationContext(cultureHolder, synchronizationImplementation.CreateCopy()); 
    } 

    public override string ToString() 
    { 
     return string.Format("CultureAwareSynchronizationContext: {0}", cultureHolder); 
    } 
} 

Usage:

/// code that detects Browser's culture 
void Detection() 
{ 
     Thread.CurrentThread.CurrentUICulture = new CultureInfo("cs"); 
     SynchronizationContext.SetSynchronizationContext(new CultureAwareSynchronizationContext()); 
} 

Questa soluzione presenta il possibile issues mentioned by Hans Passant.

+0

Questa è la soluzione migliore per ora. Si noti che esistono diversi metodi di supporto ASP.NET che * assumono * il contesto corrente è 'AspNetSynchronizationContext'. Quindi questa soluzione deve essere utilizzata con cura. –

+0

Sembra molto invasivo sostituire il contesto di sincronizzazione ASP.NET incorporato. Inoltre, questo potrebbe spezzare le ipotesi che il framework e le librerie fanno su ciò che un contesto di sincronizzazione fa o non fa. Il contesto di sincronizzazione è stato globale: fare attenzione. – usr

+0

@StephenCleary, puoi essere più specifico? Solo i riferimenti diretti a "AspNetSynchronizatonContext" che ho trovato si trovano in Page.cs - per esempio http://referencesource.microsoft.com/#System.Web/UI/Page.cs.5095 - quelli sono usati per il caricamento asincrono di ASP Controlli .NET Il mio 'SynchronizationContext' dovrebbe avere lo stesso comportamento di' SynchronizationContext' che era in uso prima (eccetto CurrentCulture) - avvolge e chiama il precedente. – nothrow

Problemi correlati