Project

General

Profile

Actions

SimpleChat made easy - Part 1

Currently the situation with Wt's Comet is that you have to track the sessions you want to push data to yourself.

I have written some classes that help dealing with this issue and simplifies building the simple chat example.

We start with coding a new application type that is derived from Wt application, a Wt application that uses my comet example should use this application class instead of the normal WApplication class.

CometApplication.hpp

#ifndef CometApplication_HPP
#define CometApplication_HPP

// <STL>
#include <list>
#include <string>
#include <map>
// </STL>

// <Boost>
#include <boost/unordered_map.hpp>
// </Boost>

// <Wt>
#include <Wt/WApplication>
#include <Wt/WWidget>
// </Wt>

namespace UI
{
    namespace Widgets
    {
                // Forward declerations
        class CometWidget;

        template <class>
        class CometWidgetFactory;
    }

    namespace Application
    {
        class CometApplication : public Wt::WApplication
        {
        public:
            CometApplication(const Wt::WEnvironment &env); // Ctor, passes enviorment variables to WApplication
            ~CometApplication(); // Dtor

            friend class Widgets::CometWidget; // Befriending with CometWidget so it can access the widget map

            template <class>
            friend class Widgets::CometWidgetFactory; // Same here
        private:
            typedef std::pair<int, Wt::WWidget *> WidgetEntryType;
            typedef boost::unordered_multimap<CometApplication *, WidgetEntryType > WidgetMapType;
            typedef boost::unordered_multimap<CometApplication *, WidgetEntryType >::iterator WidgetMapIteratorType;
            typedef std::pair<WidgetMapIteratorType, WidgetMapIteratorType> WidgetRangeType;

            static WidgetMapType widgets;

            // Pushes a new widget to the current session with group id and the widget itself
            inline void registerWidget(unsigned long int id, Wt::WWidget *w)
            {
                boost::mutex::scoped_lock  lock(CometMutex);

                widgets.insert(WidgetMapType::value_type(this, WidgetEntryType(id, w)));
            }

            // Searches for the widget of the group id for the current session and erases it
            inline void unregisterWidget(unsigned long int id)
            {
                boost::mutex::scoped_lock  lock(CometMutex);

                WidgetRangeType range = widgets.equal_range(this);

                for ( WidgetMapIteratorType iter = range.first; iter != range.second; ++iter )
                {
                    if ( iter->second.first == id )
                    {
                        widgets.erase(iter);
                        break;
                    }
                }
            }

            // Searches for a widget in the current session, casts it to the right widget type and returns it
            template <class T>
            inline T *getWidget(unsigned long int id)
            {
                boost::mutex::scoped_lock  lock(CometMutex);

                WidgetRangeType range = widgets.equal_range(this);

                for ( WidgetMapIteratorType iter = range.first; iter != range.second; ++iter )
                    if ( iter->second.first == id )
                        return dynamic_cast<T *>(iter->second.second);

                return NULL;
            }

            static boost::mutex CometMutex;
        };
    }
}

#endif

CometApplication.cpp

#include "CometApplication.hpp"

namespace UI
{
    namespace Application
    {
        CometApplication::CometApplication(const Wt::WEnvironment &env)
        : WApplication(env)
        {
            enableUpdates(); // Enables ajax-push for the current session
        }

        CometApplication::~CometApplication()
        {
            boost::mutex::scoped_lock  lock(CometMutex);

            if ( widgets.find(this) != widgets.end() ) // Erases the current session from the widget map
                widgets.erase(this);
        }

        CometApplication::WidgetMapType CometApplication::widgets;
        CometApplication::boost::mutex CometMutex;
    }
}

Code Review

I have created an unordered multimap that might look a little strange to you guys but I'm going to elaborate about it in a minute.

  1. The multimap key is the pointer to the current session which is represented by CometApplication, because as you guys already know, a session is an application instance. That way I can access the current session's widgets using this or iterate through all sessions.
  2. The value of the multimap is a pair of a group id and a widget. Different widgets in different sessions with the same group id can be accessed this way.

As you can see the CometApplication instance only refers to this, which means only to the current session.

I have befriended all other classes because registerWidget, unregisterWidget and getWidget should only be accessed by those classes.

This doesn't indicate that my code has a design problem in my opinion.

Sometimes you actually have to use class friendship.

It makes sense that the end user will not control anything related to registering/unregistering widgets as you will see.

After that we create a factory which will instanciate a widget for each session.

Here the magic is introduced into the system.

CometWidgetFactory.hpp

#ifndef CometWidgetFactory_HPP
#define CometWidgetFactory_HPP

#include <Wt/WApplication>
#include <Wt/WWidget>

#include <boost/thread/mutex.hpp>

#include "CometApplication.hpp"

namespace UI
{
    namespace Widgets
    {
        // CometWidgetFactory creates a widget of type WidgetType, WidgetType is derived from CometWidget
        template <class WidgetType>
        class CometWidgetFactory
        {
        private:
            static unsigned long int idCounter; // Counts how many ids there are

            unsigned long int id; // Group id
        public:
            CometWidgetFactory()
            : id(idCounter)
            {
                idCounter++; // New factory instance has been created.
            }

            ~CometWidgetFactory()
            {
                idCounter--; // New factory instance has been destroyed
            }

            void create()
            {
                WidgetType *w = new WidgetType; // Creates a new widget
                w->id = id; // Assigns the group id

                // Registers the widget to the current session
                dynamic_cast<UI::Application::CometApplication *>(wApp)->registerWidget(id, w); 
            }

            void destroy()
            {
                // Unregisters the widget from the current session
                dynamic_cast<UI::Application::CometApplication *>(wApp)->unregisterWidget(id);
            }

            // Those operator overloadings make the factory "point" to the current widget we're refering to in the current session
            WidgetType *operator->()
            {
                return dynamic_cast<UI::Application::CometApplication *>(wApp)->getWidget<WidgetType>(id);
            }

            operator Wt::WWidget *()
            {
                return dynamic_cast<UI::Application::CometApplication *>(wApp)->getWidget<WidgetType>(id);
            }
        };

        template <class T>
        unsigned long int CometWidgetFactory<T>::idCounter = 0;
    }
}

#endif

Code Review

This class looks very simple but it has a trick.

If you declare it as static on your target widget it will create a group id for a certain type of widgets for all sessions.

For example:

class someWidget : public CometWidget<Wt::LineEdit>
{
//...
};

class MyPage : Wt::WContainer
{
private:
  static CometWidgetFactory<someWidget> myWidget;
public:
  MyPage()
  {
    myWidget.create(); // Creates a widget on the same group id
  }

  ~MyPage()
  {
    myWidget.destroy();
  }
};

For each session a someWidget will be created and you can access the widget on the current session.

CometWidget.hpp

#ifndef CometWidget_HPP
#define CometWidget_HPP

#include <string>

#include <boost/thread/mutex.hpp>

#include <Wt/WContainerWidget>

#include "CometApplication.hpp"

namespace UI
{
    namespace Widgets
    {
        template <class WidgetType>
        class CometWidget : public WidgetType
        {
        protected:
            virtual void updateOthers()
            {
        boost::mutex::scoped_lock  lock(CometMutex);

                for ( Application::CometApplication::WidgetMapIteratorType iter = Application::CometApplication::widgets.begin(); iter != Application::CometApplication::widgets.end(); ++iter )
                {
                    Application::CometApplication::WidgetRangeType range = Application::CometApplication::widgets.equal_range(iter->first);

                    for ( Application::CometApplication::WidgetMapIteratorType iter2 = range.first; iter2 != range.second; ++iter2 )
                    {
                        if ( iter2->second.first == id )
                        {
                            Wt::WApplication::UpdateLock uiLock = iter->first->getUpdateLock();
                            dynamic_cast<CometWidget *>(iter2->second.second)->updateWidget();
                            iter->first->triggerUpdate();
                        }
                    }
                }
            }

            virtual void updateWidget() = 0; // TBD: Convert this to a signal/slot instead of a virtual function

            template <class>
            friend class CometWidgetFactory;
        private:
            unsigned long int id; // Group ID

            boost::mutex CometMutex;
        };
    }
}

#endif

Code Review

updateOthers() updates each widget in the widget group and it should be called whenever a change was made in the widget.

updateWidget should be a slot that will be connected to signals that update the widget itself.

In the next part of the article I will demonstrate how to re-write the SimpleChat example very simply.

Updated by Alex V about 6 years ago ยท 12 revisions