Disaccoppiamento del client Silverlight dalla classe generata di riferimento del servizio

StackOverflow https://stackoverflow.com/questions/617377

  •  03-07-2019
  •  | 
  •  

Domanda

Sto studiando Prism v2 passando attraverso le guide introduttive. E ho creato un servizio WCF con la seguente firma:

namespace HelloWorld.Silverlight.Web
{
[ServiceContract(Namespace = "http://helloworld.org/messaging")]
[AspNetCompatibilityRequirements(RequirementsMode =
                                 AspNetCompatibilityRequirementsMode.Allowed)]
  public class HelloWorldMessageService
  {
    private string message = "Hello from WCF";

    [OperationContract]
    public void UpdateMessage(string message)
    {
      this.message = message;
    }

    [OperationContract]
    public string GetMessage()
    {
      return message;
    }
  }
}

Quando aggiungo un riferimento di servizio a questo servizio nel mio progetto Silverlight, viene generata un'interfaccia e una classe:

[System.ServiceModel.ServiceContractAttribute
        (Namespace="http://helloworld.org/messaging",
         ConfigurationName="Web.Services.HelloWorldMessageService")]
public interface HelloWorldMessageService {

  [System.ServiceModel.OperationContractAttribute
          (AsyncPattern=true,
      Action="http://helloworld.org/messaging/HelloWorldMessageService/UpdateMessage", 
ReplyAction="http://helloworld.org/messaging/HelloWorldMessageService/UpdateMessageResponse")]
    System.IAsyncResult BeginUpdateMessage(string message, System.AsyncCallback callback, object asyncState);

    void EndUpdateMessage(System.IAsyncResult result);

    [System.ServiceModel.OperationContractAttribute(AsyncPattern=true, Action="http://helloworld.org/messaging/HelloWorldMessageService/GetMessage", ReplyAction="http://helloworld.org/messaging/HelloWorldMessageService/GetMessageResponse")]
    System.IAsyncResult BeginGetMessage(System.AsyncCallback callback, object asyncState);

    string EndGetMessage(System.IAsyncResult result);
}

public partial class HelloWorldMessageServiceClient : System.ServiceModel.ClientBase<HelloWorld.Core.Web.Services.HelloWorldMessageService>, HelloWorld.Core.Web.Services.HelloWorldMessageService {
{
    // implementation
}

Sto cercando di disaccoppiare la mia applicazione passando l'interfaccia invece della classe concrete. Ma ho difficoltà a trovare esempi su come farlo. Quando provo a chiamare EndGetMessage e quindi aggiorno l'interfaccia utente, ricevo un'eccezione sull'aggiornamento dell'interfaccia utente sul thread errato. Come posso aggiornare l'interfaccia utente da un thread in background?


Ho provato ma ottengo UnauthorizedAccessException: accesso cross-thread non valido .

string messageresult = _service.EndGetMessage(result);

Application.Current.RootVisual.Dispatcher.BeginInvoke(() => this.Message = messageresult );

L'eccezione è generata da Application.Current.RootVisual .

È stato utile?

Soluzione 6

Solo rivisitare vecchi post lasciati senza risposta dove ho finalmente trovato una risposta. Ecco un post che ho scritto di recente che spiega in dettaglio come ho finalmente gestito tutto questo:

http://www.developmentalmadness.com/archive/2009/11/04/mvvm-with-prism-101-ndash-part-6-commands.aspx

Altri suggerimenti

Ecco qualcosa che mi piace fare ... Il proxy di servizio è generato con un'interfaccia

HelloWorldClient : IHelloWorld

Ma il problema è che IHelloWorld non include le versioni Async del metodo. Quindi, creo un'interfaccia asincrona:

public interface IHelloWorldAsync : IHelloWorld
{
    void HelloWorldAsync(...);
    event System.EventHandler<HelloWorldEventRgs> HelloWorldCompleted;
}

Quindi, puoi dire al proxy del servizio di implementare l'interfaccia via parziale:

public partial class HelloWorldClient : IHelloWorldAsync {}

Poiché HelloWorldClient implementa effettivamente quei metodi asincroni, questo funziona.

Quindi, posso semplicemente usare IHelloWorldAsync ovunque e dire a UnityContainer di usare HelloWorldClient per le interfacce IHelloWorldAsync .

Ok, ho fatto casino con questo tutto il giorno e la soluzione è davvero molto più semplice di così. Inizialmente volevo chiamare i metodi sull'interfaccia anziché la classe concreate. L'interfaccia generata dal generatore di classi proxy include solo i metodi BeginXXX e EndXXX e ho ricevuto un'eccezione quando ho chiamato EndXXX .

Bene, ho appena finito di leggere su System.Threading.Dispatcher e finalmente capisco come usarlo. Dispatcher è un membro di qualsiasi classe che eredita da DispatcherObject , come fanno gli elementi dell'interfaccia utente. Il Dispatcher opera sul thread dell'interfaccia utente, che per la maggior parte delle applicazioni WPF esiste solo 1 thread dell'interfaccia utente. Ci sono eccezioni, ma credo che tu debba farlo esplicitamente in modo da sapere se lo stai facendo. Altrimenti, hai solo un singolo thread dell'interfaccia utente. Quindi è sicuro archiviare un riferimento a un Dispatcher da utilizzare in classi non UI.

Nel mio caso sto usando Prism e il mio Presenter deve aggiornare l'interfaccia utente (non direttamente, ma sta generando eventi IPropertyChanged.PropertyChanged ). Quindi quello che ho fatto è nel mio Bootstrapper quando ho impostato la shell su Application.Current.RootVisual Conservo anche un riferimento al Dispatcher come in questo modo:

public class Bootstrapper : UnityBootstrapper
{
    protected override IModuleCatalog GetModuleCatalog()
    {
    // setup module catalog
    }

    protected override DependencyObject CreateShell()
    {
        // calling Resolve instead of directly initing allows use of dependency injection
    Shell shell = Container.Resolve<Shell>();

        Application.Current.RootVisual = shell;

        Container.RegisterInstance<Dispatcher>(shell.Dispatcher);

        return shell;
    }
}

Quindi il mio presentatore ha un ctor che accetta IUnityContainer come argomento (usando DI) quindi posso fare quanto segue:

_service.BeginGetMessage(new AsyncCallback(GetMessageAsyncComplete), null);    

private void GetMessageAsyncComplete(IAsyncResult result)
{
    string output = _service.EndGetMessage(result);
    Dispatcher dispatcher = _container.Resolve<Dispatcher>();
    dispatcher.BeginInvoke(() => this.Message = output);
}

Questo è molto più semplice. Non l'ho capito prima.

Ok, quindi il mio vero problema era come disaccoppiare la mia dipendenza dalla classe proxy creata dal mio riferimento al servizio. Stavo cercando di farlo utilizzando l'interfaccia generata insieme alla classe proxy. Il che avrebbe potuto funzionare bene, ma poi avrei anche dovuto fare riferimento al progetto che possedeva il riferimento al servizio e quindi non sarebbe stato veramente disaccoppiato. Quindi ecco cosa ho finito per fare. È un po 'un trucco, ma sembra funzionare fino ad ora.

Per prima cosa ecco la mia definizione di interfaccia e una classe adattatore per gli args del gestore eventi personalizzati generati con il mio proxy:

using System.ComponentModel;

namespace HelloWorld.Interfaces.Services
{
    public class GetMessageCompletedEventArgsAdapter : System.ComponentModel.AsyncCompletedEventArgs
    {
        private object[] results;

        public GetMessageCompletedEventArgsAdapter(object[] results, System.Exception exception, bool cancelled, object userState) :
            base(exception, cancelled, userState)
        {
            this.results = results;
        }

        public string Result
        {
            get
            {
                base.RaiseExceptionIfNecessary();
                return ((string)(this.results[0]));
            }
        }
    }

    /// <summary>
    /// Create a partial class file for the service reference (reference.cs) that assigns
    /// this interface to the class - then you can use this reference instead of the
    /// one that isn't working
    /// </summary>

    public interface IMessageServiceClient
    {
        event System.EventHandler<GetMessageCompletedEventArgsAdapter> GetMessageCompleted;
        event System.EventHandler<AsyncCompletedEventArgs> UpdateMessageCompleted;

        void GetMessageAsync();
        void GetMessageAsync(object userState);

        void UpdateMessageAsync(string message);
        void UpdateMessageAsync(string message, object userState);
    }
}

Quindi ho solo bisogno di creare una classe parziale che estenda la classe proxy generata dal riferimento del servizio:

using System;

using HelloWorld.Interfaces.Services;
using System.Collections.Generic;

namespace HelloWorld.Core.Web.Services
{
    public partial class HelloWorldMessageServiceClient : IMessageServiceClient
    {

        #region IMessageServiceClient Members

        private event EventHandler<GetMessageCompletedEventArgsAdapter> handler;
        private Dictionary<EventHandler<GetMessageCompletedEventArgsAdapter>, EventHandler<GetMessageCompletedEventArgs>> handlerDictionary 
            = new Dictionary<EventHandler<GetMessageCompletedEventArgsAdapter>, EventHandler<GetMessageCompletedEventArgs>>();

        /// <remarks>
        /// This is an adapter event which allows us to apply the IMessageServiceClient
        /// interface to our MessageServiceClient. This way we can decouple our modules
        /// from the implementation
        /// </remarks>
        event EventHandler<GetMessageCompletedEventArgsAdapter> IMessageServiceClient.GetMessageCompleted
        {
            add 
            { 
                handler += value;
                EventHandler<GetMessageCompletedEventArgs> linkedhandler = new EventHandler<GetMessageCompletedEventArgs>(HelloWorldMessageServiceClient_GetMessageCompleted);
                this.GetMessageCompleted += linkedhandler;
                handlerDictionary.Add(value, linkedhandler);
            }
            remove 
            { 
                handler -= value;
                EventHandler<GetMessageCompletedEventArgs> linkedhandler = handlerDictionary[value];
                this.GetMessageCompleted -= linkedhandler;
                handlerDictionary.Remove(value);
            }
        }

        void HelloWorldMessageServiceClient_GetMessageCompleted(object sender, GetMessageCompletedEventArgs e)
        {
            if (this.handler == null)
                return;

            this.handler(sender, new GetMessageCompletedEventArgsAdapter(new object[] { e.Result }, e.Error, e.Cancelled, e.UserState));
        }

        #endregion
    }
}

Questa è un'implementazione esplicita del gestore eventi in modo che io possa concatenare gli eventi. Quando l'utente si registra per il mio evento adattatore, mi registro per l'evento effettivo generato. Quando viene generato l'evento, viene attivato l'evento dell'adattatore. Finora questo "funziona sulla mia macchina".

Passare attraverso l'interfaccia (dopo aver istanziato il client) dovrebbe essere semplicemente come usare HelloWorldMessageService invece della classe HelloWorldMessageServiceClient.

Per aggiornare l'interfaccia utente è necessario utilizzare l'oggetto Dispatcher. Ciò consente di fornire un delegato che viene invocato nel contesto del thread dell'interfaccia utente. Vedi questo post di blog per alcuni dettagli.

Puoi renderlo ancora più semplice.

Il motivo per cui il proxy funziona e la tua copia del contratto non lo è perché WCF genera il proxy con il codice che "pubblica". il callback sul thread chiamante anziché effettuare il callback sul thread in esecuzione quando viene restituita la chiamata di servizio.

Un'implementazione parziale molto semplificata, non testata, per darti l'idea di come funzionano i proxy WCF:

{
    var state = new
        {
            CallingThread = SynchronizationContext.Current,
            Callback = yourCallback
            EndYourMethod = // assign delegate
        };

    yourService.BeginYourMethod(yourParams, WcfCallback, state);
}

private void WcfCallback(IAsyncResult asyncResult)
{
    // Read the result object data to get state
    // Call EndYourMethod and block until the finished
    state.Context.Post(state.YourCallback, endYourMethodResultValue);
}

La chiave è la memorizzazione di syncronizationContext e la chiamata del metodo Post. Ciò farà sì che il callback si verifichi sullo stesso thread su cui era stato chiamato Begin. Funzionerà sempre senza coinvolgere l'oggetto Dispatcher purché tu chiami Begin dal tuo thread dell'interfaccia utente. In caso contrario, si torna al punto di partenza con l'utilizzo del Dispatcher, ma lo stesso problema si verificherà con un proxy WCF.

Questo link fa un buon lavoro nel spiegare come farlo manualmente:
http://msdn.microsoft.com/en-us /library/dd744834(VS.95).aspx

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top