Aby omówić przedstawioną w temacie kwestię opiszę krótko projekt który obecnie realizuję. Otóż jest to prosty system do automatycznej aktualizacji aplikacji. Przez weba można załadować paczkę instalacyjną, natomiast po stronie klienta działa usługa Windows Service mająca za zadanie ściągać tę paczkę i ją zainstalować. Stworzyłem również proste GUI w postaci aplikacji WPF-owej dla klienta, które komunikuje się z usługą po WCFie za pomocą named-pipes. Początkowo zamierzałem wszystkie wywołania zrobić synchronicznie i tak też zacząłem implementację: ściąganie listy aktualizacji i zarządzanie konfiguracją jest zrealizowane przez synchroniczne wywołania realizowane przez specjalnie tworzony do tego BackgroundWorker (żeby nie blokować głównego wątku GUI). Niestety, takie podejście nie sprawdza się w następnym zadaniu które chciałem wykonać, a mianowicie chciałem żeby usługa na bieżąco informowała GUI o postępie wykonywanych zadań. Czyli, żeby na GUI ładnie wyświetlała się lista aktualizacji a przy nich stan: "oczekująca", "w trakcie instalacji" itp. W takim podejściu model synchroniczny "wysiada", bo żeby komunikacja była płynna należałoby co kilkaset milisekund odpytywać usługę o to co się dzieje (tworząc za każdym razem oczywiście osobny wątek do obsługi!) co oczywiście wydajnościowo "nie wydoliłoby". Jednym sensownym rozwiązaniem jest asynchroniczna komunikacja dwustronna - klient zgłasza się, że chce otrzymywać informację od usługi a usługa bezpośrednio komunikuje się z nim tylko w momencie kiedy ma coś ciekawego do powiedzenia. Taka komunikacja jest możliwa w WCF-ie przy zastosowaniu trybu "duplex" i dodatkowo musi być odpowiedni binding (named pipes albo net-tcp). Aby cosik takiego zrealizować należy uskutecznić kilka kroków:
Różni się on od typowego kontraktu WCF-owego tym, że musi posiadać dwa dodatkowego atrybuty: SessionMode ustawione na Required oraz CallbackContract w którym należy ustawić typ (interfejs) implementowany po stronie klienta. W moim przypadku wyglądało to tak:
[ServiceContract(SessionMode=SessionMode.Required, CallbackContract = typeof(IClientCallback))]
public interface IClientAPI
{
// inne metody (synchroniczne)
[OperationContract]
void RegisterForNotification();
}
public interface IClientCallback
{
[OperationContract(IsOneWay = true)]
void UpdateInfo(UpdateState updateState);
}
Jak widać, po stronie usługi mamy jedną metodę RegisterForNotification() służąca do rejestracji klienta. Jest ona konieczna aby usługa wiedziała kogo informować o swoich wydarzeniach. Typ zdefiniowany jako callback musi implementować interfejs IClientCallback która posiada jedną metodę: UpdateInfo służąca do odswieżenia informacji o aktualizacji.
2. Zaimplementować kontrakt po stronie usługi Windows
Moja usługa Windows do komunikacji z klientem wykorzystuje WCF którego sama hostuje. Aby ten hosting był możliwy w trybie duplex musi być spełniony jeden ważny warunek: usługa WCF musi być typu Singleton. Implementacja usługi wygląda następująco:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
public class ClientAPI : IClientAPI
{
private readonly IServiceLogger _logger;
private IClientCallback _clientCallback;
public ClientAPI(IHostApplication application)
{
application.UpdateInfo += new EventHandler<UpdateInfoEventArgs>(application_UpdateInfo);
}
void application_UpdateInfo(object sender, UpdateInfoEventArgs e)
{
if (_clientCallback != null)
{
_clientCallback.UpdateInfo(e.UpdateInfo);
}
}
public void RegisterForNotification()
{
_clientCallback = OperationContext.Current.GetCallbackChannel<IClientCallback>();
}
}
Jak widać, w konstruktorze przekazujemy obiekt typu IHostApplication który reprezentuje aplikację hostującą. Potrzebujemy go, aby podpiąć się do jej eventów (co również dzieje się w konstruktorze). Po przechwyceniu eventu od aplikacji jest on przekazywany do obiektu typu IClientCallback czyli naszego klienta. W najprostszym przypadku należałoby po prostu dodać IHostApplication jako typ implementowany przez usługę ale w moim przypadku sytuacja jest trochę bardziej skomplikowana. Ponieważ do harmonogramowania i wykonywania zadań używam Quartz .NET którego Joby (czyli zadania do wykonania) są bezstanowe to potrzebowałem użyć dodatkowego, statycznego obiektu którego nazwałem NotificationHandler. Jego zadanie jest bardzo proste - implementuje interfejs IHostApplication i jest wywoływany przez poszczególne Joby Quartzowe które informują o wykonywanych zadaniach. Jego implementacja wygląda następująco:
public class NotificationHandler : IHostApplication
{
public event EventHandler<UpdateInfoEventArgs> UpdateInfo;
public void Notify(UpdateState state)
{
UpdateInfo(this, new UpdateInfoEventArgs(state));
}
}
Sam hosting takiej usługi WCF (ClientAPI) w mojej usłudze Windows wygląda następująco: (nawiasem mówiąc zbieżność terminu "usługa" może być myląca bo czasem oznacza usługę WCF a czasem usługę Windows).
public partial class VUClientService : ServiceBase
{
private ServiceHost _serviceHost;
private static NotificationHandler _notificationHandler;
public static NotificationHandler NotificationHandler { get { return _notificationHandler; } }
// trochę kodu
protected override void OnStart(string[] args)
{
// dużo kodu
try
{
if (_serviceHost != null)
{
_serviceHost.Close();
}
var clientAPI = new ClientAPI(NotificationHandler);
_serviceHost = new ServiceHost(clientAPI);
_serviceHost.Open();
_logger.LogInfo("Usługa API uruchomiona");
}
catch (Exception exception)
{
_logger.LogError("Błąd podczas inicjalizacji hosta usługi:" + exception.ToString());
}
}
}
Jak widać, usługę WCF tworzymy przekazując w konstruktorze nasz obiekt typu IHostApplication czyli NotificationHandlera. To powinno wystarczyć i nasza usługa Windows po odpaleniu rozpoczyna hosting ClientAPI. Pozostaje do implementacji strona klienta. Aha, trzeba jeszcze dopisać kilka linijek w konfiguracji:
<system.serviceModel>
<services>
<service name="VSoft.VU.ClientService.ClientAPI">
<endpoint address="net.pipe://localhost/VUClientAPI"
binding="netNamedPipeBinding"
contract="VSoft.VU.ClientServiceAPI.IClientAPI" />
</service>
</services>
</system.serviceModel>
Jak widać defniujemy binding po named-pipes o adresie localhost/VUClientAPI
3. Zaimplementować IClientCallback po stronie klienta
Mój klient jest aplikacją WPF-ową. Aby mogła ona być wywoływana przez usługę musi zaimplementować IClientCallback:
public partial class App : IClientCallback
{
public event EventHandler NotifyUpdateInfo;
public void UpdateInfo(UpdateState updateState)
{
if (NotifyUpdateInfo != null)
{
NotifyUpdateInfo(updateState, null);
}
}
}
A samo użycie eventa NotifyUpdateInfo wygląda następująco:
var updateStateModel = new UpdatesStateInfoViewModel();
var app = (App) Application.Current;
app.NotifyUpdateInfo += (o, e) => updateStateModel.UpdateInfo((UpdateState) o);
updateStateModel.RegisterForNotification();
Jeszcze klasa modelu (zgodnie ze wzorcem MVVM) UpdatesStateInfoViewModel:
public class UpdatesStateInfoViewModel : TabViewModelBase
{
public ObservableCollection<UpdateState> Updates { get; private set; }
public UpdatesStateInfoViewModel()
{
Updates = new ObservableCollection<UpdateState>();
}
public void UpdateInfo(UpdateState updateState)
{
var existing = Updates.FirstOrDefault(f => f.UpdateInfo.UpdateId == updateState.UpdateInfo.UpdateId);
if (existing == null)
{
Updates.Add(updateState);
}
else
{
Updates.Remove(existing);
Updates.Add(updateState);
}
}
public void RegisterForNotification()
{
var proxy = new ClientAPIProxy(Application.Current);
try
{
proxy.RegisterForNotification();
}
catch (Exception exc)
{
ShowMessage(exc.ToString());
}
}
}
Jak widać, po otrzymaniu zdarzenia jest wywoływana metoda UpdateInfo() która aktualizuję listę Updates (a ponieważ jest to ObservableCollection powoduje automatyczne odswieżenie widoku). Ważną rzeczą jest rejestracja klienta po stronie usługi - zajmuje się tym metoda RegisterForNotification(). Obiekt ClientAPIProxy jest to proxy wykorzystywane przy wywołaniach. Jego kod wygląda następująco:
public class ClientAPIProxy : System.ServiceModel.DuplexClientBase<IClientAPI>, IClientAPI
{
public ClientAPIProxy(object callbackInstance) : base(callbackInstance)
{
}
public UpdateInfoResult GetUpdates()
{
return Channel.GetUpdates();
}
public OperationResult AcceptUpdates(Guid[] updatesId)
{
return Channel.AcceptUpdates(updatesId);
}
public OperationResult ChangeConfiguration(ServiceConfiguration configuration)
{
return Channel.ChangeConfiguration(configuration);
}
public ConfigurationResult GetConfiguration()
{
return Channel.GetConfiguration();
}
public void RegisterForNotification()
{
Channel.RegisterForNotification();
}
}
Różni się on od "zwykłego" nie-dupleksowego proxy tym, że dziedziczy po DuplexClientBase a nie ClientBase i wymaga podania obiektu callback w konstruktorze. Aczkolwiek podanie tam null również nic złego nie spowoduje :).
Konfiguracja po stronie klienta wygląda następująco:
<system.serviceModel>
<client>
<endpoint address="net.pipe://localhost/VUClientAPI"
binding="netNamedPipeBinding"
contract="VSoft.VU.ClientServiceAPI.IClientAPI" />
</client>
</system.serviceModel>
I to by było na tyle. Podsumowując, dupleksowa komunikacja między procesami za pomocą WCF jest całkiem fajna - wymaga mniej zachodu niż ręczne pisanie obsługi named-pipesów czy gniazd nie mówiąc o jeszcze bardziej niskopoziomowych opcjach.