diff --git a/requirements.txt b/requirements.txt
index d1868e55bc17fca9bf03489f5d65044fe2129ec8..145df061d3a4c6fa1c9cb843d1b39244403a12bf 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,4 @@
 duniterpy>=0.20.dev0
 git+https://github.com/Insoleet/quamash.git@master
 asynctest
-networkx
-attr
\ No newline at end of file
+networkx
\ No newline at end of file
diff --git a/src/sakia/gui/dialogs/revocation/__init__.py b/src/sakia/gui/dialogs/revocation/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/sakia/gui/dialogs/revocation/controller.py b/src/sakia/gui/dialogs/revocation/controller.py
new file mode 100644
index 0000000000000000000000000000000000000000..37a25b2fdb1b9e5519e2864ccf9103da7b92e810
--- /dev/null
+++ b/src/sakia/gui/dialogs/revocation/controller.py
@@ -0,0 +1,132 @@
+from sakia.gui.component.controller import ComponentController
+from .view import RevocationView
+from .model import RevocationModel
+from duniterpy.documents import MalformedDocumentError
+from sakia.tools.decorators import asyncify
+import asyncio
+
+
+class RevocationController(ComponentController):
+    """
+    The revocation view
+    """
+
+    def __init__(self, parent, view, model):
+        """
+        Constructor of the revocation component
+
+        :param sakia.gui.revocation.view.revocationView: the view
+        :param sakia.gui.revocation.model.revocationModel model: the model
+        """
+        super().__init__(parent, view, model)
+
+        self.handle_next_step(init=True)
+        self.view.button_next.clicked.connect(lambda checked: self.handle_next_step(False))
+        self._steps = (
+            {
+                'page': self.ui.page_load_file,
+                'init': self.init_dialog,
+                'next': self.revocation_selected
+            },
+            {
+                'page': self.ui.page_destination,
+                'init': self.init_publication_page,
+                'next': self.publish
+            }
+        )
+        self._current_step = 0
+
+    @classmethod
+    def create(cls, parent, app, **kwargs):
+        """
+        Instanciate a revocation component
+        :param sakia.gui.component.controller.ComponentController parent:
+        :param sakia.core.Application app:
+        :return: a new revocation controller
+        :rtype: revocationController
+        """
+        account = kwargs['account']
+        view = RevocationView(parent.view)
+        model = RevocationModel(None, app, account)
+        revocation = cls(parent, view, model)
+        model.setParent(revocation)
+        return revocation
+
+    @classmethod
+    def open_dialog(cls, parent, app, account):
+        """
+        Certify and identity
+        :param sakia.gui.component.controller.ComponentController parent: the parent
+        :param sakia.core.Application app: the application
+        :param sakia.core.Account account: the account certifying the identity
+        :return:
+        """
+        dialog = cls.create(parent, app, account=account)
+        dialog.refresh()
+        return dialog.exec()
+
+    @property
+    def view(self) -> RevocationView:
+        return self._view
+
+    @property
+    def model(self) -> RevocationModel:
+        return self._model
+
+    def handle_next_step(self, init=False):
+        if self._current_step < len(self._steps) - 1:
+            if not init:
+                self.view.button_next.clicked.disconnect(self._steps[self._current_step]['next'])
+                self._current_step += 1
+            self._steps[self._current_step]['init']()
+            self.view.stackedWidget.setCurrentWidget(self._steps[self._current_step]['page'])
+            self.view.button_next.clicked.connect(self._steps[self._current_step]['next'])
+
+    def load_from_file(self):
+        selected_file = self.view.select_revocation_file()
+        try:
+            self.model.load_revocation(selected_file)
+        except FileNotFoundError:
+            pass
+        except MalformedDocumentError:
+            self.view.malformed_file_error()
+            self.button_next.setEnabled(False)
+
+    def revocation_selected(self):
+        pass
+
+    def init_publication_page(self):
+        communities_names = self.model.communities_names()
+        self.view.set_communities_names(communities_names)
+
+    def publish(self):
+        self.view.button_next.setEnabled(False)
+        if self.view.ask_for_confirmation():
+            self.accept()
+        else:
+            self.view.button_next.setEnabled(True)
+
+    @asyncify
+    async def accept(self):
+        if self.ui.radio_community.isChecked():
+            index = self.view.combo_community.currentIndex()
+            result, error = await self.model.send_to_community(index)
+        else:
+            server = self.ui.edit_address.text()
+            port = self.ui.spinbox_port.value()
+            result, error = await self.model.send_to_node(server, port)
+
+        if result:
+            self.view.accept()
+        else:
+            await self.view.revocation_broadcast_error(error)
+
+    def async_exec(self):
+        future = asyncio.Future()
+        self.widget.finished.connect(lambda r: future.set_result(r))
+        self.widget.open()
+        self.refresh()
+        return future
+
+    def exec(self):
+        self.widget.exec()
diff --git a/src/sakia/gui/dialogs/revocation/model.py b/src/sakia/gui/dialogs/revocation/model.py
new file mode 100644
index 0000000000000000000000000000000000000000..8647369bed24cd4d5efea375be2fbb7108e02709
--- /dev/null
+++ b/src/sakia/gui/dialogs/revocation/model.py
@@ -0,0 +1,67 @@
+from sakia.gui.component.model import ComponentModel
+from duniterpy.documents.certification import Revocation
+from duniterpy.api import bma, errors
+from sakia.core.net import Node
+import aiohttp
+
+
+class RevocationModel(ComponentModel):
+    """
+    The model of HomeScreen component
+    """
+
+    def __init__(self, parent, app, account):
+        super().__init__(parent)
+        self.app = app
+        self.account = account
+
+        self.revocation_document = None
+        self.revoked_selfcert = None
+
+    def load_revocation(self, path):
+        """
+        Load a revocation document from a file
+        :param str path:
+        """
+        with open(path, 'r') as file:
+            file_content = file.read()
+            self.revocation_document = Revocation.from_signed_raw(file_content)
+            self.revoked_selfcert = Revocation.extract_self_cert(file_content)
+
+    def communities_names(self):
+        return [c.name for c in self.account.communities]
+
+    async def send_to_community(self, index):
+        community = self.account.communities[index]
+        responses = await community.bma_access.broadcast(bma.wot.Revoke, {},
+                                       {
+                                           'revocation': self.revocation_document.signed_raw(
+                                               self.revoked_selfcert)
+                                       })
+
+        result = False, ""
+        for r in responses:
+            if r.status == 200:
+                result = True, (await r.json())
+            elif not result[0]:
+                result = False, (await r.text())
+            else:
+                await r.release()
+
+        return result
+
+    async def send_to_node(self, server, port):
+        session = aiohttp.ClientSession()
+        try:
+            node = await Node.from_address(None, server, port, session=session)
+            conn_handler = node.endpoint.conn_handler()
+            await bma.wot.Revoke(conn_handler).post(session,
+                                                    revocation=self.revocation_document.signed_raw(
+                                                        self.revoked_selfcert))
+        except (ValueError, errors.DuniterError,
+                aiohttp.errors.ClientError, aiohttp.errors.DisconnectedError,
+                aiohttp.errors.TimeoutError) as e:
+            return False, str(e)
+        finally:
+            session.close()
+        return True, ""
diff --git a/res/ui/revocation.ui b/src/sakia/gui/dialogs/revocation/revocation.ui
similarity index 97%
rename from res/ui/revocation.ui
rename to src/sakia/gui/dialogs/revocation/revocation.ui
index d7edc4990e334b82dbe71405c249084bd05ad234..21641ab85afbae8571c4904f07b91166987cbf16 100644
--- a/res/ui/revocation.ui
+++ b/src/sakia/gui/dialogs/revocation/revocation.ui
@@ -166,6 +166,13 @@ QGroupBox::title {
             </property>
            </widget>
           </item>
+          <item>
+           <widget class="QLabel" name="label_target">
+            <property name="text">
+             <string/>
+            </property>
+           </widget>
+          </item>
           <item>
            <spacer name="verticalSpacer">
             <property name="orientation">
diff --git a/src/sakia/gui/dialogs/revocation/view.py b/src/sakia/gui/dialogs/revocation/view.py
new file mode 100644
index 0000000000000000000000000000000000000000..d296bf4e472246e69fb8f33b91925ec2d69ea907
--- /dev/null
+++ b/src/sakia/gui/dialogs/revocation/view.py
@@ -0,0 +1,105 @@
+from PyQt5.QtWidgets import QDialog, QFileDialog, QMessageBox
+from .revocation_uic import Ui_RevocationDialog
+from enum import Enum
+from sakia.gui.widgets.dialogs import QAsyncMessageBox
+from sakia.tools.decorators import asyncify
+
+
+class RevocationView(QDialog, Ui_RevocationDialog):
+    """
+    Home screen view
+    """
+
+    class PublicationMode(Enum):
+        ADDRESS = 0
+        COMMUNITY = 1
+
+    def __init__(self, parent):
+        """
+        Constructor
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+
+        self.button_next.setEnabled(False)
+        self.button_load.clicked.connect(self.load_from_file)
+
+        self.radio_address.toggled.connect(lambda c: self.publication_mode_changed(RevocationView.PublicationMode.ADDRESS))
+        self.radio_community.toggled.connect(lambda c: self.publication_mode_changed(RevocationView.PublicationMode.COMMUNITY))
+        self.edit_address.textChanged.connect(self.refresh)
+        self.spinbox_port.valueChanged.connect(self.refresh)
+        self.combo_community.currentIndexChanged.connect(self.refresh)
+
+    def publication_mode_changed(self, radio):
+        self.edit_address.setEnabled(radio == RevocationView.PublicationMode.ADDRESS)
+        self.spinbox_port.setEnabled(radio == RevocationView.PublicationMode.ADDRESS)
+        self.combo_community.setEnabled(radio == RevocationView.PublicationMode.COMMUNITY)
+        self.refresh_revocation_label()
+
+    def refresh_target(self):
+        if self.radio_community.isChecked():
+            target = self.tr(
+                "All nodes of community {name}".format(name=self.combo_community.currentText()))
+        elif self.radio_address.isChecked():
+            target = self.tr("Address {address}:{port}".format(address=self.edit_address.text(),
+                                                               port=self.spinbox_port.value()))
+        else:
+            target = ""
+        self.label_target.setText("""
+<h4>Publication address</h4>
+<div>{target}</div>
+""".format(target=target))
+
+    def select_revocation_file(self):
+        """
+        Get a revocation file using a file dialog
+        :rtype: str
+        """
+        selected_files = QFileDialog.getOpenFileName(self.widget,
+                                    self.tr("Load a revocation file"),
+                                    "",
+                                    self.tr("All text files (*.txt)"))
+        selected_file = selected_files[0]
+        return selected_file
+
+    def malformed_file_error(self):
+        QMessageBox.critical(self, self.tr("Error loading document"),
+                             self.tr("Loaded document is not a revocation document"),
+                             buttons=QMessageBox.Ok)
+
+    async def revocation_broadcast_error(self, error):
+        await QAsyncMessageBox.critical(self, self.tr("Error broadcasting document"),
+                                        error)
+
+    def show_revoked_selfcert(self, selfcert):
+        text = self.tr("""
+        <div>Identity revoked : {uid} (public key : {pubkey}...)</div>
+        <div>Identity signed on block : {timestamp}</div>
+            """.format(uid=selfcert.uid,
+                       pubkey=selfcert.pubkey[:12],
+                       timestamp=selfcert.timestamp))
+        self.label_revocation_content.setText(text)
+
+    def set_communities_names(self, names):
+        self.combo_community.clear()
+        for name in names:
+            self.combo_community.addItem(name)
+        self.radio_community.setChecked(True)
+
+    def ask_for_confirmation(self):
+        answer = QMessageBox.warning(self.widget, self.tr("Revocation"),
+                                     self.tr("""<h4>The publication of this document will remove your identity from the network.</h4>
+        <li>
+            <li> <b>This identity won't be able to join the targeted community anymore.</b> </li>
+            <li> <b>This identity won't be able to generate Universal Dividends anymore.</b> </li>
+            <li> <b>This identity won't be able to certify individuals anymore.</b> </li>
+        </li>
+        Please think twice before publishing this document.
+        """), QMessageBox.Ok | QMessageBox.Cancel)
+        return answer == QMessageBox.Ok
+
+    @asyncify
+    async def accept(self):
+        await QAsyncMessageBox.information(self.widget, self.tr("Revocation broadcast"),
+                                     self.tr("The document was successfully broadcasted."))
+        super().accept()
diff --git a/src/sakia/gui/revocation.py b/src/sakia/gui/revocation.py
deleted file mode 100644
index 31b3d6b31beb8d14df7a67f1372334eb5bf3911b..0000000000000000000000000000000000000000
--- a/src/sakia/gui/revocation.py
+++ /dev/null
@@ -1,208 +0,0 @@
-import asyncio
-import aiohttp
-import logging
-from duniterpy.api import errors
-from duniterpy.api import bma
-from duniterpy.documents import MalformedDocumentError
-from duniterpy.documents.certification import Revocation
-from sakia.core.net import Node
-from PyQt5.QtWidgets import QDialog, QFileDialog, QMessageBox
-from PyQt5.QtCore import QObject
-
-from .widgets.dialogs import QAsyncMessageBox
-from ..tools.decorators import asyncify
-from ..presentation.revocation_uic import Ui_RevocationDialog
-
-
-class RevocationDialog(QObject):
-    """
-    A dialog to revoke an identity
-    """
-
-    def __init__(self, app, account, widget, ui):
-        """
-        Constructor if a certification dialog
-
-        :param sakia.core.Application app:
-        :param sakia.core.Account account:
-        :param PyQt5.QtWidgets widget: the widget of the dialog
-        :param sakia.gen_resources.revocation_uic.Ui_RevocationDialog ui: the view of the certification dialog
-        :return:
-        """
-        super().__init__()
-        self.widget = widget
-        self.ui = ui
-        self.ui.setupUi(self.widget)
-        self.app = app
-        self.account = account
-        self.revocation_document = None
-        self.revoked_selfcert = None
-        self._steps = (
-            {
-                'page': self.ui.page_load_file,
-                'init': self.init_dialog,
-                'next': self.revocation_selected
-            },
-            {
-                'page': self.ui.page_destination,
-                'init': self.init_publication_page,
-                'next': self.publish
-            }
-        )
-        self._current_step = 0
-        self.handle_next_step(init=True)
-        self.ui.button_next.clicked.connect(lambda checked: self.handle_next_step(False))
-
-    def handle_next_step(self, init=False):
-        if self._current_step < len(self._steps) - 1:
-            if not init:
-                self.ui.button_next.clicked.disconnect(self._steps[self._current_step]['next'])
-                self._current_step += 1
-            self._steps[self._current_step]['init']()
-            self.ui.stackedWidget.setCurrentWidget(self._steps[self._current_step]['page'])
-            self.ui.button_next.clicked.connect(self._steps[self._current_step]['next'])
-
-    def init_dialog(self):
-        self.ui.button_next.setEnabled(False)
-        self.ui.button_load.clicked.connect(self.load_from_file)
-
-        self.ui.radio_address.toggled.connect(lambda c: self.publication_mode_changed("address"))
-        self.ui.radio_community.toggled.connect(lambda c: self.publication_mode_changed("community"))
-        self.ui.edit_address.textChanged.connect(self.refresh)
-        self.ui.spinbox_port.valueChanged.connect(self.refresh)
-        self.ui.combo_community.currentIndexChanged.connect(self.refresh)
-
-    def publication_mode_changed(self, radio):
-        self.ui.edit_address.setEnabled(radio == "address")
-        self.ui.spinbox_port.setEnabled(radio == "address")
-        self.ui.combo_community.setEnabled(radio == "community")
-        self.refresh()
-
-    def load_from_file(self):
-        selected_files = QFileDialog.getOpenFileName(self.widget,
-                                          self.tr("Load a revocation file"),
-                                          "",
-                                          self.tr("All text files (*.txt)"))
-        selected_file = selected_files[0]
-        try:
-            with open(selected_file, 'r') as file:
-                file_content = file.read()
-                self.revocation_document = Revocation.from_signed_raw(file_content)
-                self.revoked_selfcert = Revocation.extract_self_cert(file_content)
-                self.refresh()
-                self.ui.button_next.setEnabled(True)
-        except FileNotFoundError:
-            pass
-        except MalformedDocumentError:
-            QMessageBox.critical(self.widget, self.tr("Error loading document"),
-                                        self.tr("Loaded document is not a revocation document"),
-                                 QMessageBox.Ok)
-            self.ui.button_next.setEnabled(False)
-
-    def revocation_selected(self):
-        pass
-
-    def init_publication_page(self):
-        self.ui.combo_community.clear()
-        if self.account:
-            for community in self.account.communities:
-                self.ui.combo_community.addItem(community.currency)
-            self.ui.radio_community.setChecked(True)
-        else:
-            self.ui.radio_address.setChecked(True)
-            self.ui.radio_community.setEnabled(False)
-
-    def publish(self):
-        self.ui.button_next.setEnabled(False)
-        answer = QMessageBox.warning(self.widget, self.tr("Revocation"),
-                                      self.tr("""<h4>The publication of this document will remove your identity from the network.</h4>
-<li>
-    <li> <b>This identity won't be able to join the targeted community anymore.</b> </li>
-    <li> <b>This identity won't be able to generate Universal Dividends anymore.</b> </li>
-    <li> <b>This identity won't be able to certify individuals anymore.</b> </li>
-</li>
-Please think twice before publishing this document.
-"""), QMessageBox.Ok | QMessageBox.Cancel)
-        if answer == QMessageBox.Ok:
-            self.accept()
-        else:
-            self.ui.button_next.setEnabled(True)
-
-    @asyncify
-    async def accept(self):
-        try:
-            session = aiohttp.ClientSession()
-            if self.ui.radio_community.isChecked():
-                community = self.account.communities[self.ui.combo_community.currentIndex()]
-                await community.bma_access.broadcast(bma.wot.Revoke, {},
-                       {
-                           'revocation': self.revocation_document.signed_raw(self.revoked_selfcert)
-                       })
-            elif self.ui.radio_address.isChecked():
-                    server = self.ui.edit_address.text()
-                    port = self.ui.spinbox_port.value()
-                    node = await Node.from_address(None, server, port, session=session)
-                    conn_handler = node.endpoint.conn_handler()
-                    await bma.wot.Revoke(conn_handler).post(session,
-                                                revocation=self.revocation_document.signed_raw(self.revoked_selfcert))
-        except (MalformedDocumentError, ValueError, errors.DuniterError,
-            aiohttp.errors.ClientError, aiohttp.errors.DisconnectedError,
-                aiohttp.errors.TimeoutError) as e:
-            await QAsyncMessageBox.critical(self.widget, self.tr("Error broadcasting document"),
-                                        str(e))
-        else:
-            await QAsyncMessageBox.information(self.widget, self.tr("Revocation broadcast"),
-                                               self.tr("The document was successfully broadcasted."))
-            self.widget.accept()
-        finally:
-            session.close()
-
-    @classmethod
-    def open_dialog(cls, app, account):
-        """
-        Certify and identity
-        :param sakia.core.Application app: the application
-        :param sakia.core.Account account: the account certifying the identity
-        :return:
-        """
-        dialog = cls(app, account, QDialog(), Ui_RevocationDialog())
-        dialog.refresh()
-        return dialog.exec()
-
-    def refresh(self):
-        if self.revoked_selfcert:
-            text = self.tr("""
-<div>Identity revoked : {uid} (public key : {pubkey}...)</div>
-<div>Identity signed on block : {timestamp}</div>
-    """.format(uid=self.revoked_selfcert.uid,
-               pubkey=self.revoked_selfcert.pubkey[:12],
-               timestamp=self.revoked_selfcert.timestamp))
-
-            self.ui.label_revocation_content.setText(text)
-
-            if self.ui.radio_community.isChecked():
-                target = self.tr("All nodes of community {name}".format(name=self.ui.combo_community.currentText()))
-            elif self.ui.radio_address.isChecked():
-                target = self.tr("Address {address}:{port}".format(address=self.ui.edit_address.text(),
-                                                                   port=self.ui.spinbox_port.value()))
-            else:
-                target = ""
-            self.ui.label_revocation_info.setText("""
-<h4>Revocation document</h4>
-<div>{text}</div>
-<h4>Publication address</h4>
-<div>{target}</div>
-""".format(text=text,
-           target=target))
-        else:
-            self.ui.label_revocation_content.setText("")
-
-    def async_exec(self):
-        future = asyncio.Future()
-        self.widget.finished.connect(lambda r: future.set_result(r))
-        self.widget.open()
-        self.refresh()
-        return future
-
-    def exec(self):
-        self.widget.exec()