Découplage du client Silverlight de la classe générée par la référence de service

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

  •  03-07-2019
  •  | 
  •  

Question

Je fais des recherches sur Prism v2 en parcourant les quickstarts. Et j'ai créé un service WCF avec la signature suivante:

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;
    }
  }
}

Lorsque j'ajoute une référence de service à ce service dans mon projet silverlight, il génère une interface et une 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
}

J'essaie de découpler mon application en transmettant l'interface à la place de la classe concrète. Mais j'ai du mal à trouver des exemples sur la façon de procéder. Lorsque j'essaie d'appeler EndGetMessage puis de mettre à jour mon interface utilisateur, je reçois une exception concernant la mise à jour de l'interface utilisateur sur le mauvais thread. Comment puis-je mettre à jour l'interface utilisateur à partir d'un thread en arrière-plan?

J'ai essayé mais j'obtiens UnauthorizedAccessException: accès multithread non valide .

string messageresult = _service.EndGetMessage(result);

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

L'exception est levée par Application.Current.RootVisual .

Était-ce utile?

La solution 6

Il suffit de revenir sur d’anciennes publications laissées sans réponse, où j’ai enfin trouvé une réponse. Voici un article que j'ai écrit récemment et qui explique en détail comment j'ai finalement géré tout cela:

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

Autres conseils

Voici quelque chose que j'aime faire ... Le proxy de service est généré avec une interface

HelloWorldClient : IHelloWorld

Mais le problème est que IHelloWorld n'inclut pas les versions asynchrones de la méthode. Donc, je crée une interface asynchrone:

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

Ensuite, vous pouvez indiquer au proxy de service de mettre en œuvre l'interface via partial:

public partial class HelloWorldClient : IHelloWorldAsync {}

Etant donné que HelloWorldClient implémente effectivement ces méthodes asynchrones, cela fonctionne.

Ensuite, je peux simplement utiliser IHelloWorldAsync et dire au UnityContainer d'utiliser HelloWorldClient pour les interfaces IHelloWorldAsync .

Ok, je me suis amusé à ça toute la journée et la solution est vraiment beaucoup plus simple que cela. Au départ, je voulais appeler les méthodes sur l'interface au lieu de la classe concreate. L'interface générée par le générateur de classes proxy inclut uniquement les méthodes BeginXXX et EndXXX et j'avais une exception lorsque j'ai appelé EndXXX .

Eh bien, je viens de terminer la lecture de System.Threading.Dispatcher et j'ai enfin compris comment l'utiliser. Dispatcher est membre de toute classe qui hérite de DispatcherObject , comme le font les éléments de l'interface utilisateur. Dispatcher fonctionne sur le thread d'interface utilisateur qui, pour la plupart des applications WPF, ne comporte qu'un seul thread d'interface utilisateur. Il y a des exceptions, mais je crois que vous devez le faire explicitement pour savoir si vous le faites. Sinon, vous n'avez qu'un seul thread d'interface utilisateur. Il est donc prudent de stocker une référence à un répartiteur pour une utilisation dans des classes non-UI.

Dans mon cas, j'utilise Prism et mon présentateur doit mettre à jour l'interface utilisateur (pas directement mais il déclenche des événements IPropertyChanged.PropertyChanged ). Donc, ce que j'ai fait est dans mon Bootstrapper lorsque je règle le shell sur Application.Current.RootVisual , je stocke également une référence au Dispatcher comme ceci:

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;
    }
}

Ensuite, mon présentateur a un acteur qui accepte IUnityContainer en tant qu’argument (avec DI), puis je peux effectuer les opérations suivantes:

_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);
}

C'est tellement beaucoup plus simple. Je n'avais tout simplement pas compris avant.

OK, mon vrai problème était donc de savoir comment découpler ma dépendance de la classe de proxy créée par ma référence de service. J'essayais de faire cela en utilisant l'interface générée avec la classe proxy. Ce qui aurait pu bien fonctionner, mais j'aurais aussi dû faire référence au projet qui possédait la référence de service et ainsi, il ne serait pas vraiment découplé. Alors voici ce que j'ai fini par faire. C'est un peu un bidouillage, mais cela semble fonctionner jusqu'à présent.

Voici d'abord ma définition d'interface et une classe d'adaptateur pour les arguments du gestionnaire d'événements personnalisés générés avec mon 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);
    }
}

Ensuite, je devais simplement créer une classe partielle qui étend la classe proxy générée par la référence de service:

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
    }
}

Il s'agit d'une implémentation explicite du gestionnaire d'événements afin que je puisse enchaîner les événements. Lorsque l'utilisateur s'inscrit pour mon événement adaptateur, je m'inscris pour l'événement déclenché. Lorsque l'événement se déclenche, je déclenche mon événement adaptateur. Jusqu'à présent, cette "Fonctionne sur ma machine".

La transmission de l'interface (une fois le client instancié) doit être aussi simple que d'utiliser HelloWorldMessageService au lieu de la classe HelloWorldMessageServiceClient.

Pour mettre à jour l'interface utilisateur, vous devez utiliser l'objet Dispatcher. Cela vous permet de fournir un délégué appelé dans le contexte du thread d'interface utilisateur. Consultez cet article de blog pour plus de détails.

Vous pouvez encore simplifier les choses.

Si le proxy fonctionne et que votre copie du contrat ne fonctionne pas, c'est parce que WCF génère le proxy avec le code "Posts". rappel sur le thread appelant plutôt que de le rappeler sur le thread en cours d’exécution au retour de l’appel de service.

Une implémentation partielle très simplifiée, non testée, pour vous donner une idée du fonctionnement des mandataires WCF, ressemble à ceci:

{
    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 clé est le stockage du syncronizationContext et l’appel de la méthode Post. Cela fera que le rappel se produise sur le même thread que celui sur lequel Begin a été appelé. Cela fonctionnera toujours sans impliquer l'objet Dispatcher à condition d'appeler Begin depuis votre thread d'interface utilisateur. Si vous ne le faites pas, vous revenez à la case départ avec l'utilisation de Dispatcher, mais le même problème se produira avec un proxy WCF.

Ce lien explique bien comment procéder manuellement:
http://msdn.microsoft.com/en-us /library/dd744834(VS.95).aspx

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top