changeset 2326:f5d2e4b76439 refactoring

SdlSimpleViewer using the new observer/observable pattern
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 30 Sep 2026 21:15:01 +0200
parents 27b730ba6d4d
children 139c0ed1ed0c
files Applications/Samples/Sdl/SingleFrameViewer/SdlSimpleViewer.cpp Applications/Samples/WebAssembly/SingleFrameViewer/SingleFrameViewerApplication.h Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp OrthancStone/Sources/Loaders/DicomResourcesLoader.cpp OrthancStone/Sources/Loaders/DicomResourcesLoader.h OrthancStone/Sources/Loaders/DicomVolumeLoader.cpp OrthancStone/Sources/Loaders/DicomVolumeLoader.h OrthancStone/Sources/Loaders/SeriesFramesLoader.cpp OrthancStone/Sources/Loaders/SeriesFramesLoader.h OrthancStone/Sources/Loaders/SeriesMetadataLoader.cpp OrthancStone/Sources/Loaders/SeriesMetadataLoader.h OrthancStone/Sources/Messages/IObservable.h OrthancStone/Sources/Messages/TypedObservable.h OrthancStone/Sources/Messages/TypedObserver.h
diffstat 14 files changed, 372 insertions(+), 201 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/Samples/Sdl/SingleFrameViewer/SdlSimpleViewer.cpp	Wed Sep 30 20:06:19 2026 +0200
+++ b/Applications/Samples/Sdl/SingleFrameViewer/SdlSimpleViewer.cpp	Wed Sep 30 21:15:01 2026 +0200
@@ -20,6 +20,8 @@
  **/
 
 
+#include "../../../../OrthancStone/Sources/Messages/TypedObserver.h"
+
 #include "../../../../OrthancStone/Sources/Loaders/DicomResourcesLoader.h"
 #include "../../../../OrthancStone/Sources/Loaders/SeriesFramesLoader.h"
 #include "../../../../OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.h"
@@ -75,8 +77,7 @@
     LOG(ERROR) << "error!";
   }
 };
-
-static boost::shared_ptr<Toto> toto_(new Toto);   // TODO REMOVE
+// END TODO Refactoring
 
 
 namespace OrthancStone
@@ -93,13 +94,13 @@
 
     virtual bool HandleKeyDown(char key) = 0;
 
-    virtual void HandleMouseDown(OrthancStone::IViewport::ILock& lock,
-                                 const OrthancStone::PointerEvent& p) = 0;
+    virtual void HandleMouseDown(IViewport::ILock& lock,
+                                 const PointerEvent& p) = 0;
 
-    virtual bool HandleMouseMove(OrthancStone::IViewport::ILock& lock,
-                                 const OrthancStone::PointerEvent& p) = 0;
+    virtual bool HandleMouseMove(IViewport::ILock& lock,
+                                 const PointerEvent& p) = 0;
 
-    virtual void Render(OrthancStone::IViewport::ILock& lock) = 0;
+    virtual void Render(IViewport::ILock& lock) = 0;
   };
 
 
@@ -146,7 +147,7 @@
                    (event.window.event == SDL_WINDOWEVENT_SHOWN ||
                     event.window.event == SDL_WINDOWEVENT_EXPOSED))
           {
-            std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
+            std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
             lock->RefreshCanvasSize();
           }
           else if (event.type == SDL_TEXTINPUT)
@@ -174,11 +175,11 @@
                    event.type == SDL_MOUSEMOTION ||
                    event.type == SDL_MOUSEBUTTONUP)
           {
-            std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
+            std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
 
             if (lock->HasCompositor())
             {
-              OrthancStone::PointerEvent p;
+              PointerEvent p;
               OrthancStoneHelpers::GetPointerEvent(p, lock->GetCompositor(), event, keyboardState, scancodeCount);
 
               switch (event.type)
@@ -206,7 +207,7 @@
         if (paint)
         {
           {
-            std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
+            std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
             core_->Render(*lock);
           }
 
@@ -235,18 +236,18 @@
 
       if (useOpenGL)
       {
-        viewport_ = OrthancStone::SdlOpenGLViewport::Create(title, width, height);
+        viewport_ = SdlOpenGLViewport::Create(title, width, height);
       }
       else
       {
-        viewport_ = OrthancStone::SdlCairoViewport::Create(title, width, height);
+        viewport_ = SdlCairoViewport::Create(title, width, height);
       }
 
       std::string font;
       Orthanc::EmbeddedResources::GetFileResource(font, Orthanc::EmbeddedResources::UBUNTU_FONT);
 
       {
-        std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
+        std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
         lock->GetCompositor().SetFont(0, font, 16, Orthanc::Encoding_Latin1);
       }
     }
@@ -254,7 +255,9 @@
 
 
   class SimpleViewerApp : public ISingleViewportApplicationCore,
-                          public ObserverBase<SimpleViewerApp>  // TODO Refactoring - Remove
+                          public boost::enable_shared_from_this<SimpleViewerApp>,
+                          public New::TypedObserver<FrameDecodedMessage>,
+                          public New::TypedObserver<DicomResourcesLoadedMessage>
   {
   private:
     std::string                                   instanceId_;
@@ -262,13 +265,15 @@
     boost::shared_ptr<StoneApplication::Context>  context_;
     boost::shared_ptr<IViewport>                  viewport_;
     AnnotationsSceneLayer                         annotations_;
-    OrthancStone::DefaultViewportInteractor       interactor_;
+    DefaultViewportInteractor                     interactor_;
     boost::shared_ptr<Toto>                       toto_;
     boost::shared_ptr<DicomResourcesLoader>       dicomLoader_;
     boost::shared_ptr<SeriesFramesLoader>         framesLoader_;
-    OrthancStone::Units                           units_;
+    Units                                         units_;
 
-    void Handle(const SeriesFramesLoader::FrameLoadedMessage& message)
+  public:
+    virtual void Handle(const New::IObservable& observable,
+                        const FrameDecodedMessage& message) ORTHANC_OVERRIDE
     {
       LOG(INFO) << "Frame decoded! "
                 << message.GetImage().GetWidth() << "x" << message.GetImage().GetHeight()
@@ -287,14 +292,15 @@
       }
     }
 
-    void Handle(const DicomResourcesLoader::SuccessMessage& message)
+    virtual void Handle(const New::IObservable& observable,
+                        const DicomResourcesLoadedMessage& message) ORTHANC_OVERRIDE
     {
       if (message.GetResources()->GetSize() != 1)
       {
         throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
       }
 
-      OrthancStone::DicomInstanceParameters parameters(message.GetResources()->GetResource(0));
+      DicomInstanceParameters parameters(message.GetResources()->GetResource(0));
       if (parameters.HasPixelSpacing())
       {
         /**
@@ -309,7 +315,7 @@
         // std::cout << message.GetResources()->GetSourceJson(0).toStyledString();
 
         LOG(INFO) << "Using millimeters units, as the DICOM instance contains the PixelSpacing tag";
-        units_ = OrthancStone::Units_Millimeters;
+        units_ = Units_Millimeters;
       }
       else
       {
@@ -319,11 +325,12 @@
       //message.GetResources()->GetResource(0).Print(stdout);
 
       {
-        std::unique_ptr<OrthancStone::IEnvironment::ILock> lock(context_->GetEnvironment().AcquireLock());
+        std::unique_ptr<IEnvironment::ILock> lock(context_->GetEnvironment().AcquireLock());
 
         framesLoader_ = SeriesFramesLoader::Create(*context_, *message.GetResources());
 
-        Register<SeriesFramesLoader::FrameLoadedMessage>(*framesLoader_, &SimpleViewerApp::Handle);
+        //Register<FrameDecodedMessage>(*framesLoader_, &SimpleViewerApp::Handle);
+        framesLoader_->Register(shared_from_this());
 
         assert(message.HasUserPayload());
 
@@ -348,9 +355,12 @@
 
       dicomLoader_ = DicomResourcesLoader::Create(*context);
 
-      Register<DicomResourcesLoader::SuccessMessage>(*dicomLoader_, &SimpleViewerApp::Handle);
+      //Register<DicomResourcesLoadedMessage>(*dicomLoader_, &SimpleViewerApp::Handle);
 
-      annotations_.SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_Edit);
+      //New::TypedObservable<DicomResourcesLoadedMessage>::Bind(*dicomLoader_, shared_from_this());
+      dicomLoader_->Register(shared_from_this());
+
+      annotations_.SetActiveTool(AnnotationsSceneLayer::Tool_Edit);
       annotations_.SetProbedLayer(0);
 
       interactor_.SetWindowingLayer(0);
@@ -358,13 +368,13 @@
       toto_.reset(new Toto);
 
       {
-        std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport->Lock());
-        lock->GetController().SetUndoStack(boost::make_shared<OrthancStone::UndoStack>());
+        std::unique_ptr<IViewport::ILock> lock(viewport->Lock());
+        lock->GetController().SetUndoStack(boost::make_shared<UndoStack>());
       }
 
       {
-        // std::unique_ptr<OrthancStone::IEnvironment::ILock> lock(context->GetEnvironment().AcquireLock());
-        OrthancStone::DicomSource source;
+        // std::unique_ptr<IEnvironment::ILock> lock(context->GetEnvironment().AcquireLock());
+        DicomSource source;
         dicomLoader_->ScheduleLoadOrthancResource(boost::make_shared<LoadedDicomResources>(Orthanc::DICOM_TAG_SOP_INSTANCE_UID),
                                                   0, source, Orthanc::ResourceType_Instance, instanceId_,
                                                   new Orthanc::SingleValueObject<unsigned int>(frameIndex_));
@@ -378,19 +388,19 @@
         case 'b':
         {
           // TODO Refactoring
-          OrthancStone::IEnvironment& environment = context_->GetEnvironment();
-          OrthancStone::IOracle& oracle = context_->GetOracle();
+          IEnvironment& environment = context_->GetEnvironment();
+          IOracle& oracle = context_->GetOracle();
 
-          oracle.Submit(environment, toto_, new OrthancStone::SleepOracleCommand(1000));
+          oracle.Submit(environment, toto_, new SleepOracleCommand(1000));
 
           {
-            std::unique_ptr<OrthancStone::HttpCommand> command(new OrthancStone::HttpCommand);
+            std::unique_ptr<HttpCommand> command(new HttpCommand);
             command->SetUrl("http://ip-api.com/json/");
             oracle.Submit(environment, toto_, command.release());
           }
 
           {
-            std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
+            std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
             command->SetUri("/system/");
             oracle.Submit(environment, toto_, command.release());
           }
@@ -399,7 +409,7 @@
           {
             DicomSource source;
             source.SetDicomDirSource();
-            oracle.Submit(environment, toto_, new OrthancStone::ParseDicomFromFileCommand(source, "hand.dcm"));
+            oracle.Submit(environment, toto_, new ParseDicomFromFileCommand(source, "hand.dcm"));
           }
           break;
         }
@@ -414,7 +424,7 @@
 
         case 'u':
         {
-          std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
+          std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
           if (lock->GetController().CanUndo())
           {
             lock->GetController().Undo();
@@ -424,7 +434,7 @@
 
         case 'U':
         {
-          std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
+          std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
           if (lock->GetController().CanRedo())
           {
             lock->GetController().Redo();
@@ -433,35 +443,35 @@
         }
 
         case 'c':
-          annotations_.SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_Circle);
+          annotations_.SetActiveTool(AnnotationsSceneLayer::Tool_Circle);
           break;
 
         case 'm':
-          annotations_.SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_Edit);
+          annotations_.SetActiveTool(AnnotationsSceneLayer::Tool_Edit);
           break;
 
         case 'd':
-          annotations_.SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_Remove);
+          annotations_.SetActiveTool(AnnotationsSceneLayer::Tool_Remove);
           break;
 
         case 'l':
-          annotations_.SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_Length);
+          annotations_.SetActiveTool(AnnotationsSceneLayer::Tool_Length);
           break;
 
         case 'a':
-          annotations_.SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_Angle);
+          annotations_.SetActiveTool(AnnotationsSceneLayer::Tool_Angle);
           break;
 
         case 'p':
-          annotations_.SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_PixelProbe);
+          annotations_.SetActiveTool(AnnotationsSceneLayer::Tool_PixelProbe);
           break;
 
         case 'e':
-          annotations_.SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_EllipseProbe);
+          annotations_.SetActiveTool(AnnotationsSceneLayer::Tool_EllipseProbe);
           break;
 
         case 'r':
-          annotations_.SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_RectangleProbe);
+          annotations_.SetActiveTool(AnnotationsSceneLayer::Tool_RectangleProbe);
           break;
 
         default:
@@ -471,14 +481,14 @@
       return false;  // No need to repaint
     }
 
-    virtual void HandleMouseDown(OrthancStone::IViewport::ILock& lock,
-                                 const OrthancStone::PointerEvent& p) ORTHANC_OVERRIDE
+    virtual void HandleMouseDown(IViewport::ILock& lock,
+                                 const PointerEvent& p) ORTHANC_OVERRIDE
     {
       annotations_.SetUnits(units_);
 
-      boost::shared_ptr<OrthancStone::IFlexiblePointerTracker> t;
+      boost::shared_ptr<IFlexiblePointerTracker> t;
 
-      if (p.GetMouseButton() == OrthancStone::MouseButton_Left)
+      if (p.GetMouseButton() == MouseButton_Left)
       {
         t.reset(annotations_.CreateTracker(p.GetMainPosition(), lock.GetController().GetScene()));
       }
@@ -497,8 +507,8 @@
       lock.Invalidate();
     }
 
-    virtual bool HandleMouseMove(OrthancStone::IViewport::ILock& lock,
-                                 const OrthancStone::PointerEvent& p) ORTHANC_OVERRIDE
+    virtual bool HandleMouseMove(IViewport::ILock& lock,
+                                 const PointerEvent& p) ORTHANC_OVERRIDE
     {
       if (lock.GetController().HandleMouseMove(p))
       {
@@ -519,7 +529,7 @@
       return false;
     }
 
-    virtual void Render(OrthancStone::IViewport::ILock& lock)
+    virtual void Render(IViewport::ILock& lock)
     {
       annotations_.Render(lock.GetController().GetScene());
     }
@@ -530,12 +540,11 @@
       instanceId_(instanceId),
       frameIndex_(frameIndex),
       annotations_(10),
-      units_(OrthancStone::Units_Pixels)
+      units_(Units_Pixels)
     {
     }
   };
 }
-// END TODO Refactoring
 
 
 
--- a/Applications/Samples/WebAssembly/SingleFrameViewer/SingleFrameViewerApplication.h	Wed Sep 30 20:06:19 2026 +0200
+++ b/Applications/Samples/WebAssembly/SingleFrameViewer/SingleFrameViewerApplication.h	Wed Sep 30 21:15:01 2026 +0200
@@ -49,7 +49,7 @@
     {
     }
 
-    void Handle(const SeriesFramesLoader::FrameLoadedMessage& message)
+    void Handle(const FrameDecodedMessage& message)
     {
       LOG(INFO) << "Frame decoded! "
                 << message.GetImage().GetWidth() << "x" << message.GetImage().GetHeight()
@@ -67,7 +67,7 @@
       }
     }
 
-    void Handle(const DicomResourcesLoader::SuccessMessage& message)
+    void Handle(const DicomResourcesLoadedMessage& message)
     {
       if (message.GetResources()->GetSize() != 1)
       {
@@ -80,7 +80,7 @@
         std::unique_ptr<OrthancStone::IEnvironment::ILock> lock(context_.GetEnvironment().AcquireLock());
 
         framesLoader_ = SeriesFramesLoader::Create(context_, *message.GetResources());
-        Register<SeriesFramesLoader::FrameLoadedMessage>(*framesLoader_, &SingleFrameViewerApplication::Handle);
+        Register<FrameDecodedMessage>(*framesLoader_, &SingleFrameViewerApplication::Handle);
 
         assert(message.HasUserPayload());
         const Orthanc::SingleValueObject<unsigned int>& payload =
@@ -102,7 +102,7 @@
 
       application->dicomLoader_ = DicomResourcesLoader::Create(context);
 
-      application->Register<DicomResourcesLoader::SuccessMessage>(*application->dicomLoader_, &SingleFrameViewerApplication::Handle);
+      application->Register<DicomResourcesLoadedMessage>(*application->dicomLoader_, &SingleFrameViewerApplication::Handle);
 
       return application;
     }
@@ -208,7 +208,7 @@
       }
     }
     
-    void HandleLoadedResources(const DicomResourcesLoader::SuccessMessage& message)
+    void HandleLoadedResources(const DicomResourcesLoadedMessage& message)
     {
       LoadedDicomResources series(Orthanc::DICOM_TAG_SERIES_INSTANCE_UID);
 
@@ -313,7 +313,7 @@
       application->Register<OrthancRestApiCommand::SuccessMessage>(
         context.GetOracleObservable(), &WebViewerLoaders::HandleOrthancRestApi);
 
-      application->Register<DicomResourcesLoader::SuccessMessage>(
+      application->Register<DicomResourcesLoadedMessage>(
         *application->resourcesLoader_, &WebViewerLoaders::HandleLoadedResources);
 
       application->Register<SeriesThumbnailsLoader::SuccessMessage>(
--- a/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Wed Sep 30 20:06:19 2026 +0200
+++ b/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Wed Sep 30 21:15:01 2026 +0200
@@ -780,7 +780,7 @@
   {
   }
 
-  void Handle(const OrthancStone::DicomResourcesLoader::SuccessMessage& message)
+  void Handle(const OrthancStone::DicomResourcesLoadedMessage& message)
   {
     const Orthanc::SingleValueObject<Orthanc::ResourceType>& payload =
       dynamic_cast<const Orthanc::SingleValueObject<Orthanc::ResourceType>&>(message.GetUserPayload());
@@ -1138,7 +1138,7 @@
     loader->thumbnailsLoader_ = OrthancStone::SeriesThumbnailsLoader::Create(context, PRIORITY_LOW);
     loader->metadataLoader_ = OrthancStone::SeriesMetadataLoader::Create(context);
     
-    loader->Register<OrthancStone::DicomResourcesLoader::SuccessMessage>(
+    loader->Register<OrthancStone::DicomResourcesLoadedMessage>(
       *loader->resourcesLoader_, &ResourcesLoader::Handle);
 
     loader->Register<OrthancStone::SeriesThumbnailsLoader::SuccessMessage>(
@@ -2346,7 +2346,7 @@
       return *viewport_;
     }
     
-    virtual void Handle(const OrthancStone::DicomResourcesLoader::SuccessMessage& message) const
+    virtual void Handle(const OrthancStone::DicomResourcesLoadedMessage& message) const
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
     }
@@ -2370,7 +2370,7 @@
     {
     }
     
-    virtual void Handle(const OrthancStone::DicomResourcesLoader::SuccessMessage& message) const ORTHANC_OVERRIDE
+    virtual void Handle(const OrthancStone::DicomResourcesLoadedMessage& message) const ORTHANC_OVERRIDE
     {
       if (message.GetResources()->GetSize() != 1)
       {
@@ -3331,7 +3331,7 @@
     return true;
   }
 
-  void Handle(const OrthancStone::DicomResourcesLoader::SuccessMessage& message)
+  void Handle(const OrthancStone::DicomResourcesLoadedMessage& message)
   {
     dynamic_cast<const ICommand&>(message.GetUserPayload()).Handle(message);
   }
@@ -3446,7 +3446,7 @@
 
     {
       viewport->loader_ = OrthancStone::DicomResourcesLoader::Create(context);
-      viewport->Register<OrthancStone::DicomResourcesLoader::SuccessMessage>(
+      viewport->Register<OrthancStone::DicomResourcesLoadedMessage>(
         *viewport->loader_, &ViewerViewport::Handle);
 
       viewport->Register<OrthancStone::HttpCommand::SuccessMessage>(
--- a/OrthancStone/Sources/Loaders/DicomResourcesLoader.cpp	Wed Sep 30 20:06:19 2026 +0200
+++ b/OrthancStone/Sources/Loaders/DicomResourcesLoader.cpp	Wed Sep 30 21:15:01 2026 +0200
@@ -95,8 +95,10 @@
 
     void BroadcastSuccess()
     {
-      SuccessMessage message(*loader_, target_, priority_, source_, userPayload_.get());
+      DicomResourcesLoadedMessage message(*loader_, target_, priority_, source_, userPayload_.get());
       loader_->BroadcastMessage(message);
+
+      loader_->Notify(message);
     }
 
     boost::shared_ptr<DicomResourcesLoader> GetLoader()
@@ -593,7 +595,7 @@
     
     
 
-  const Orthanc::IDynamicObject& DicomResourcesLoader::SuccessMessage::GetUserPayload() const
+  const Orthanc::IDynamicObject& DicomResourcesLoadedMessage::GetUserPayload() const
   {
     if (userPayload_ == NULL)
     {
@@ -609,13 +611,13 @@
   boost::shared_ptr<DicomResourcesLoader> DicomResourcesLoader::Create(StoneApplication::Context& stone)
   {
     boost::shared_ptr<DicomResourcesLoader> result(new DicomResourcesLoader(stone));
-    result->Register<HttpCommand::SuccessMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle);
-    result->Register<OracleCommandExceptionMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle);
-    result->Register<OrthancRestApiCommand::SuccessMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle);
-    result->Register<ReadFileCommand::SuccessMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle);
+    result->ObserverBase<DicomResourcesLoader>::Register<HttpCommand::SuccessMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle);
+    result->ObserverBase<DicomResourcesLoader>::Register<OracleCommandExceptionMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle);
+    result->ObserverBase<DicomResourcesLoader>::Register<OrthancRestApiCommand::SuccessMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle);
+    result->ObserverBase<DicomResourcesLoader>::Register<ReadFileCommand::SuccessMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle);
 
 #if ORTHANC_ENABLE_DCMTK == 1
-    result->Register<ParseDicomSuccessMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle);
+    result->ObserverBase<DicomResourcesLoader>::Register<ParseDicomSuccessMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle);
 #endif
     
     return result;
--- a/OrthancStone/Sources/Loaders/DicomResourcesLoader.h	Wed Sep 30 20:06:19 2026 +0200
+++ b/OrthancStone/Sources/Loaders/DicomResourcesLoader.h	Wed Sep 30 21:15:01 2026 +0200
@@ -29,6 +29,7 @@
 #  error The macro ORTHANC_ENABLE_DCMTK must be defined
 #endif
 
+#include "../Messages/TypedObservable.h"
 #include "../Oracle/HttpCommand.h"
 #include "../Oracle/OracleCommandExceptionMessage.h"
 #include "../Oracle/OrthancRestApiCommand.h"
@@ -49,11 +50,67 @@
 #if ORTHANC_ENABLE_DCMTK == 1
   class ParseDicomFromFileCommand;
 #endif
-  
+
+  class DicomResourcesLoader;
+
+  // TODO Refactoring - Move to a separate file
+  class DicomResourcesLoadedMessage : public OriginMessage<DicomResourcesLoader>
+  {
+    ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+
+  private:
+    boost::shared_ptr<LoadedDicomResources>  resources_;
+    int                                      priority_;
+    const DicomSource&                       source_;
+    const Orthanc::IDynamicObject*           userPayload_;
+
+  public:
+    DicomResourcesLoadedMessage(const DicomResourcesLoader& origin,
+                                boost::shared_ptr<LoadedDicomResources> resources,
+                                int priority,
+                                const DicomSource& source,
+                                const Orthanc::IDynamicObject* userPayload) :
+      OriginMessage(origin),
+      resources_(resources),
+      priority_(priority),
+      source_(source),
+      userPayload_(userPayload)
+    {
+    }
+
+    int GetPriority() const
+    {
+      return priority_;
+    }
+
+    const boost::shared_ptr<LoadedDicomResources> GetResources() const
+    {
+      return resources_;
+    }
+
+    const DicomSource& GetDicomSource() const
+    {
+      return source_;
+    }
+
+    bool HasUserPayload() const
+    {
+      return userPayload_ != NULL;
+    }
+
+    const Orthanc::IDynamicObject& GetUserPayload() const;
+  };
+
+
   class DicomResourcesLoader :
     public ObserverBase<DicomResourcesLoader>,
-    public IObservable
+    public IObservable,
+    public New::IObservable,
+    public New::TypedObservable<DicomResourcesLoadedMessage>
   {
+  public:
+    using New::TypedObservable<DicomResourcesLoadedMessage>::Register;  // TODO Refactoring - Presumably not needed
+
   private:
     class Handler;
     class StringHandler;
@@ -103,54 +160,6 @@
 
 
   public:
-    class SuccessMessage : public OriginMessage<DicomResourcesLoader>
-    {
-      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
-      
-    private:
-      boost::shared_ptr<LoadedDicomResources>  resources_;
-      int                                      priority_;
-      const DicomSource&                       source_;
-      const Orthanc::IDynamicObject*           userPayload_;
-      
-    public:
-      SuccessMessage(const DicomResourcesLoader& origin,
-                     boost::shared_ptr<LoadedDicomResources> resources,
-                     int priority,
-                     const DicomSource& source,
-                     const Orthanc::IDynamicObject* userPayload) :
-        OriginMessage(origin),
-        resources_(resources),
-        priority_(priority),
-        source_(source),
-        userPayload_(userPayload)
-      {
-      }
-
-      int GetPriority() const
-      {
-        return priority_;
-      }
-
-      const boost::shared_ptr<LoadedDicomResources> GetResources() const
-      {
-        return resources_;
-      }
-
-      const DicomSource& GetDicomSource() const
-      {
-        return source_;
-      }
-
-      bool HasUserPayload() const
-      {
-        return userPayload_ != NULL;
-      }
-
-      const Orthanc::IDynamicObject& GetUserPayload() const;
-    };
-
-
     static boost::shared_ptr<DicomResourcesLoader> Create(StoneApplication::Context& stone);
 
     void ScheduleGetDicomWeb(boost::shared_ptr<LoadedDicomResources> target,
--- a/OrthancStone/Sources/Loaders/DicomVolumeLoader.cpp	Wed Sep 30 20:06:19 2026 +0200
+++ b/OrthancStone/Sources/Loaders/DicomVolumeLoader.cpp	Wed Sep 30 21:15:01 2026 +0200
@@ -84,7 +84,7 @@
   }
 
 
-  void DicomVolumeLoader::Handle(const SeriesFramesLoader::FrameLoadedMessage& message)
+  void DicomVolumeLoader::Handle(const FrameDecodedMessage& message)
   {
     if (remaining_ == 0 ||
         !message.HasUserPayload())
--- a/OrthancStone/Sources/Loaders/DicomVolumeLoader.h	Wed Sep 30 20:06:19 2026 +0200
+++ b/OrthancStone/Sources/Loaders/DicomVolumeLoader.h	Wed Sep 30 21:15:01 2026 +0200
@@ -43,7 +43,7 @@
     DicomVolumeLoader(boost::shared_ptr<SeriesFramesLoader>& framesLoader,
                       bool computeRange);
 
-    void Handle(const SeriesFramesLoader::FrameLoadedMessage& message);
+    void Handle(const FrameDecodedMessage& message);
 
   public:
     class VolumeReadyMessage : public OriginMessage<DicomVolumeLoader>
--- a/OrthancStone/Sources/Loaders/SeriesFramesLoader.cpp	Wed Sep 30 20:06:19 2026 +0200
+++ b/OrthancStone/Sources/Loaders/SeriesFramesLoader.cpp	Wed Sep 30 21:15:01 2026 +0200
@@ -145,8 +145,9 @@
                << image.GetHeight() << ", " << Orthanc::EnumerationToString(image.GetFormat())
                << ", quality " << payload.GetQuality();
       
-    FrameLoadedMessage message(*this, frameIndex, payload.GetQuality(), image, instance, parameters, payload.GetUserPayload());
+    FrameDecodedMessage message(*this, frameIndex, payload.GetQuality(), image, instance, parameters, payload.GetUserPayload());
     BroadcastMessage(message);
+    Notify(message);
   }
 
 
@@ -291,7 +292,7 @@
   }
 
 
-  Orthanc::IDynamicObject& SeriesFramesLoader::FrameLoadedMessage::GetUserPayload() const
+  Orthanc::IDynamicObject& FrameDecodedMessage::GetUserPayload() const
   {
     if (userPayload_)
     {
@@ -321,13 +322,13 @@
     boost::shared_ptr<SeriesFramesLoader> loader(
       new SeriesFramesLoader(context, instances, dicomDirPath, dicomDir));
 
-    loader->Register<GetOrthancImageCommand::SuccessMessage>(context.GetOracleObservable(), &SeriesFramesLoader::Handle);
-    loader->Register<GetOrthancWebViewerJpegCommand::SuccessMessage>(context.GetOracleObservable(), &SeriesFramesLoader::Handle);
-    loader->Register<HttpCommand::SuccessMessage>(context.GetOracleObservable(), &SeriesFramesLoader::Handle);
-    loader->Register<OrthancRestApiCommand::SuccessMessage>(context.GetOracleObservable(), &SeriesFramesLoader::Handle);
+    loader->ObserverBase<SeriesFramesLoader>::Register<GetOrthancImageCommand::SuccessMessage>(context.GetOracleObservable(), &SeriesFramesLoader::Handle);
+    loader->ObserverBase<SeriesFramesLoader>::Register<GetOrthancWebViewerJpegCommand::SuccessMessage>(context.GetOracleObservable(), &SeriesFramesLoader::Handle);
+    loader->ObserverBase<SeriesFramesLoader>::Register<HttpCommand::SuccessMessage>(context.GetOracleObservable(), &SeriesFramesLoader::Handle);
+    loader->ObserverBase<SeriesFramesLoader>::Register<OrthancRestApiCommand::SuccessMessage>(context.GetOracleObservable(), &SeriesFramesLoader::Handle);
 
 #if ORTHANC_ENABLE_DCMTK == 1
-    loader->Register<ParseDicomSuccessMessage>(context.GetOracleObservable(), &SeriesFramesLoader::Handle);
+    loader->ObserverBase<SeriesFramesLoader>::Register<ParseDicomSuccessMessage>(context.GetOracleObservable(), &SeriesFramesLoader::Handle);
 #endif
 
     return loader;
--- a/OrthancStone/Sources/Loaders/SeriesFramesLoader.h	Wed Sep 30 20:06:19 2026 +0200
+++ b/OrthancStone/Sources/Loaders/SeriesFramesLoader.h	Wed Sep 30 21:15:01 2026 +0200
@@ -34,11 +34,83 @@
 #include "SeriesOrderedFrames.h"
 
 namespace OrthancStone
-{  
+{
+  class SeriesFramesLoader;
+
+  // TODO Refactoring - Move to a separate file
+  class FrameDecodedMessage : public OriginMessage<SeriesFramesLoader>
+  {
+    ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+
+  private:
+    size_t                          frameIndex_;
+    unsigned int                    quality_;
+    const Orthanc::ImageAccessor&   image_;
+    const Orthanc::DicomMap&        instance_;
+    const DicomInstanceParameters&  parameters_;
+    Orthanc::IDynamicObject*        userPayload_; // Ownership is maintained by the caller
+
+  public:
+    FrameDecodedMessage(const SeriesFramesLoader& loader,
+                        size_t frameIndex,
+                        unsigned int quality,
+                        const Orthanc::ImageAccessor& image,
+                        const Orthanc::DicomMap& instance,
+                        const DicomInstanceParameters&  parameters,
+                        Orthanc::IDynamicObject* userPayload) :
+      OriginMessage(loader),
+      frameIndex_(frameIndex),
+      quality_(quality),
+      image_(image),
+      instance_(instance),
+      parameters_(parameters),
+      userPayload_(userPayload)
+    {
+    }
+
+    size_t GetFrameIndex() const
+    {
+      return frameIndex_;
+    }
+
+    unsigned int GetQuality() const
+    {
+      return quality_;
+    }
+
+    const Orthanc::ImageAccessor& GetImage() const
+    {
+      return image_;
+    }
+
+    const Orthanc::DicomMap& GetInstance() const
+    {
+      return instance_;
+    }
+
+    const DicomInstanceParameters& GetInstanceParameters() const
+    {
+      return parameters_;
+    }
+
+    bool HasUserPayload() const
+    {
+      return userPayload_ != NULL;
+    }
+
+    Orthanc::IDynamicObject& GetUserPayload() const;
+  };
+
+
   class SeriesFramesLoader : 
     public ObserverBase<SeriesFramesLoader>,
-    public IObservable
+    public IObservable,
+    public New::IObservable,
+    public New::TypedObservable<FrameDecodedMessage>
   {
+  public:
+    using New::TypedObservable<FrameDecodedMessage>::Register;  // TODO Refactoring - Presumably not needed
+
   private:
     class Payload;
 
@@ -77,70 +149,6 @@
     void Handle(const HttpCommand::SuccessMessage& message);
 
   public:
-    class FrameLoadedMessage : public OriginMessage<SeriesFramesLoader>
-    {
-      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
-
-    private:
-      size_t                          frameIndex_;
-      unsigned int                    quality_;
-      const Orthanc::ImageAccessor&   image_;
-      const Orthanc::DicomMap&        instance_;
-      const DicomInstanceParameters&  parameters_;
-      Orthanc::IDynamicObject*        userPayload_; // Ownership is maintained by the caller
-
-    public:
-      FrameLoadedMessage(const SeriesFramesLoader& loader,
-                         size_t frameIndex,
-                         unsigned int quality,
-                         const Orthanc::ImageAccessor& image,
-                         const Orthanc::DicomMap& instance,
-                         const DicomInstanceParameters&  parameters,
-                         Orthanc::IDynamicObject* userPayload) :
-        OriginMessage(loader),
-        frameIndex_(frameIndex),
-        quality_(quality),
-        image_(image),
-        instance_(instance),
-        parameters_(parameters),
-        userPayload_(userPayload)
-      {
-      }
-
-      size_t GetFrameIndex() const
-      {
-        return frameIndex_;
-      }
-
-      unsigned int GetQuality() const
-      {
-        return quality_;
-      }
-
-      const Orthanc::ImageAccessor& GetImage() const
-      {
-        return image_;
-      }
-
-      const Orthanc::DicomMap& GetInstance() const
-      {
-        return instance_;
-      }
-
-      const DicomInstanceParameters& GetInstanceParameters() const
-      {
-        return parameters_;
-      }
-
-      bool HasUserPayload() const
-      {
-        return userPayload_ != NULL;
-      }
-
-      Orthanc::IDynamicObject& GetUserPayload() const;
-    };
-
-
     static boost::shared_ptr<SeriesFramesLoader> Create(StoneApplication::Context& context,
                                                         LoadedDicomResources& instances);
 
--- a/OrthancStone/Sources/Loaders/SeriesMetadataLoader.cpp	Wed Sep 30 20:06:19 2026 +0200
+++ b/OrthancStone/Sources/Loaders/SeriesMetadataLoader.cpp	Wed Sep 30 21:15:01 2026 +0200
@@ -52,7 +52,7 @@
   }
 
 
-  void SeriesMetadataLoader::Handle(const DicomResourcesLoader::SuccessMessage& message)
+  void SeriesMetadataLoader::Handle(const DicomResourcesLoadedMessage& message)
   {
     assert(message.GetResources());
 
@@ -194,7 +194,7 @@
     boost::shared_ptr<DicomResourcesLoader> loader(DicomResourcesLoader::Create(context));
       
     boost::shared_ptr<SeriesMetadataLoader> obj(new SeriesMetadataLoader(loader));
-    obj->Register<DicomResourcesLoader::SuccessMessage>(*loader, &SeriesMetadataLoader::Handle);
+    obj->Register<DicomResourcesLoadedMessage>(*loader, &SeriesMetadataLoader::Handle);
     return obj;
   }
 
--- a/OrthancStone/Sources/Loaders/SeriesMetadataLoader.h	Wed Sep 30 20:06:19 2026 +0200
+++ b/OrthancStone/Sources/Loaders/SeriesMetadataLoader.h	Wed Sep 30 21:15:01 2026 +0200
@@ -55,7 +55,7 @@
     bool IsScheduledWithHigherPriority(const std::string& seriesInstanceUid,
                                        int priority) const;
 
-    void Handle(const DicomResourcesLoader::SuccessMessage& message);
+    void Handle(const DicomResourcesLoadedMessage& message);
 
   public:
     class SuccessMessage : public OriginMessage<SeriesMetadataLoader>
--- a/OrthancStone/Sources/Messages/IObservable.h	Wed Sep 30 20:06:19 2026 +0200
+++ b/OrthancStone/Sources/Messages/IObservable.h	Wed Sep 30 21:15:01 2026 +0200
@@ -26,6 +26,7 @@
 #include "../StoneEnumerations.h"
 #include "ICallable.h"
 #include "IObserver.h"
+#include "TypedObservable.h"
 
 #include <set>
 #include <map>
@@ -53,4 +54,22 @@
     void EmitMessage(boost::weak_ptr<IObserver> observer,
                      const IMessage& message);
   };
+
+
+  namespace New
+  {
+    class IObservable : public boost::noncopyable
+    {
+    public:
+      virtual ~IObservable()
+      {
+      }
+
+      template <typename Message>
+      void Notify(const Message& message)
+      {
+        dynamic_cast< TypedObservable<Message>& >(*this).Dispatch(*this, message);
+      }
+    };
+  }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Messages/TypedObservable.h	Wed Sep 30 21:15:01 2026 +0200
@@ -0,0 +1,77 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "TypedObserver.h"
+
+#include <boost/shared_ptr.hpp>
+#include <boost/weak_ptr.hpp>
+#include <list>
+
+namespace OrthancStone
+{
+  namespace New
+  {
+    template <typename Message>
+    class TypedObservable : public boost::noncopyable
+    {
+    private:
+      typedef std::list< boost::weak_ptr< TypedObserver<Message> > >  Content;
+
+      Content content_;
+
+    public:
+      void Register(const boost::shared_ptr< TypedObserver<Message> >& observer)
+      {
+        content_.push_back(observer);
+      }
+
+      void Dispatch(const IObservable& observable,
+                    const Message& message)
+      {
+        Content active;
+
+        for (typename Content::const_iterator it = content_.begin(); it != content_.end(); ++it)
+        {
+          boost::shared_ptr< TypedObserver<Message> > locked = it->lock();
+
+          if (locked)
+          {
+            locked->Handle(observable, message);
+            active.push_back(locked);
+          }
+        }
+
+        content_.swap(active);
+      }
+
+      template <typename Observable>
+      static void Bind(Observable& observable,
+                       const boost::shared_ptr< TypedObserver<Message> >& observer)
+      {
+        static_cast< TypedObservable<Message>& >(observable).Register(observer);
+      }
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Messages/TypedObserver.h	Wed Sep 30 21:15:01 2026 +0200
@@ -0,0 +1,46 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <boost/noncopyable.hpp>
+
+namespace OrthancStone
+{
+  namespace New
+  {
+    class IObservable;
+
+    template <typename Message>
+    class TypedObserver : public boost::noncopyable
+    {
+    public:
+      virtual ~TypedObserver()
+      {
+      }
+
+      virtual void Handle(const IObservable& source,
+                          const Message& message) = 0;
+    };
+  }
+}