Separating Model from View/Controller

classic Classic list List threaded Threaded
3 messages Options
Reply | Threaded
Open this post in threaded view
|

Separating Model from View/Controller

ericjmcd
My colleagues are adamant about separating the Model code from the View/Controller code so I'd like to have completely separate classes/files. The Doc says that you can specify a View within a Handler class but that appears to require passing in the context to the Handler and for the configure_traits call to be on the Handler instance which becomes confusing/impossible? when dealing with classes that have instances that include other instances.

We basically have a tree of base model classes. Every class will be subclassed to implement specific notification handlers and potentially add more attributes. Each class should have a view that includes items from the base class, 'custom' items added in the subclassed class and items related to the UI only (e.g. buttons, etc. that the Handler deals with). So in the end, I'd like something like:
class Base(tr.HasTraits):
   base_item=...
class BaseHandler(tr.Handler):
   base_h_item=...
   view=View(items from base and base handler)

class Subclassed(Base):
   sub_item=...
class SubHandler(BaseHandler):
   sub_h_item=...
   view=View(items from base, base_handler, sub, and sub_handler)

b=Subclassed()
b.configure_traits()

I don't have a problem creating a Controller except that the classes are nested and I don't know how to have a top level Controller include a specific controller for an attribute (instance) that is a child of the top level model.  i.e. How do you automatically include Controllers from the top of the tree down to the leaves? Making it harder is that the specific branches/leaves of the tree will be determined at runtime.
class A():
   item=tr.Any()
class B():
   a=tr.Instance(A)
class C():
   b=tr.List(tr.Instance(B)
and so on.

This is what I have now (views mixed with model and no room for View-only objects and custom handler control):
import traits.api as tr
import traitsui.api as trui

class ABase(tr.HasTraits):
    a_base_item = tr.Str('a_base_item')    
    base_grp = trui.Group('a_base_item')
    view = trui.View(trui.VGroup(trui.Include('base_grp'),
                                 trui.Include('custom_grp') # easy way to include any subclassed contributions
                                 )
                     )

class ASubclassed(ABase):
    a_sub_item = tr.Str('a_sub_item')
    custom_grp = trui.Group('a_sub_item')
    
class BBase(tr.HasTraits):
    b_base_item = tr.Instance(ABase)
    
    base_grp = trui.Group(trui.Item('b_base_item',style='custom'))
    view = trui.View(trui.VGroup(trui.Include('base_grp'),
                                 trui.Include('custom_grp') # easy way to include any subclassed contributions
                                 )
                     )

class BSubclassed(BBase):
    b_sub_item = tr.Str('b_sub_item')    
    custom_grp = trui.Group('b_sub_item')

inst = BSubclassed()
inst.b_base_item = ASubclassed()

inst.configure_traits()


I'd like to get it to be more like the following where there is very little in the model that has View/Controller references.

# no idea on trait naming here so I'm just using object and handler for clarity
class ABaseHandler(tr.Handler):
    btn = tr.Button('Action')
    base_grp = trui.Group('object.a_base_item') 
    handler_grp = trui.Group('handler.btn')
    view = trui.View(trui.VGroup(trui.Include('base_grp'),
                                 trui.Include('object.custom_grp'), # if object is subclassed, it can contribute via a custom group
                                 trui.Include('handler_grp'),
                                 )
                     )

class ABase(tr.HasTraits):
    a_base_item = tr.Str('a_base_item')    
    default_handler = ABaseHandler() #This doesn't work but would be nice if it did

Thanks for any help and guidance you can provide,
Eric



_______________________________________________
Enthought-Dev mailing list
[hidden email]
https://mail.enthought.com/mailman/listinfo/enthought-dev
Reply | Threaded
Open this post in threaded view
|

Re: Separating Model from View/Controller

Corran Webster
Hi Eric,

a quick reply that may not fully address your question:

On Thu, Apr 11, 2013 at 1:06 AM, Eric McDonald <[hidden email]> wrote:
My colleagues are adamant about separating the Model code from the View/Controller code so I'd like to have completely separate classes/files. The Doc says that you can specify a View within a Handler class but that appears to require passing in the context to the Handler and for the configure_traits call to be on the Handler instance which becomes confusing/impossible? when dealing with classes that have instances that include other instances.

You should think of views that are defined in a HasTraits class as being "default views" if that helps with your colleagues.  You can always separate the view definition from the class itself:

class MyModel(HasTraits):
    my_trait = Float

and then possibly in a completely different file:

my_view = View(
   Item('my_trait')
)

and then when you invoke the editor you just need to be explicit about who gets what:

my_model_instance.configure_traits(view=my_view)

or

my_view.configure_traits(context={...})

or similar: you don't need an explicit Handler or Controller if the View is simple.
 
We basically have a tree of base model classes. Every class will be subclassed to implement specific notification handlers and potentially add more attributes. Each class should have a view that includes items from the base class, 'custom' items added in the subclassed class and items related to the UI only (e.g. buttons, etc. that the Handler deals with). So in the end, I'd like something like:
class Base(tr.HasTraits):
   base_item=...
class BaseHandler(tr.Handler):
   base_h_item=...
   view=View(items from base and base handler)

class Subclassed(Base):
   sub_item=...
class SubHandler(BaseHandler):
   sub_h_item=...
   view=View(items from base, base_handler, sub, and sub_handler)

b=Subclassed()
b.configure_traits()

I don't have a problem creating a Controller except that the classes are nested and I don't know how to have a top level Controller include a specific controller for an attribute (instance) that is a child of the top level model.  i.e. How do you automatically include Controllers from the top of the tree down to the leaves? Making it harder is that the specific branches/leaves of the tree will be determined at runtime.

This can be tricky, but views can specify a handler to be used with them if you need it.  I'm not sure if this works if the view is embedded in a larger view using an InstanceEditor, however.
 
class A():
   item=tr.Any()
class B():
   a=tr.Instance(A)
class C():
   b=tr.List(tr.Instance(B)
and so on.

This is what I have now (views mixed with model and no room for View-only objects and custom handler control):
import traits.api as tr
import traitsui.api as trui

class ABase(tr.HasTraits):
    a_base_item = tr.Str('a_base_item')    
    base_grp = trui.Group('a_base_item')
    view = trui.View(trui.VGroup(trui.Include('base_grp'),
                                 trui.Include('custom_grp') # easy way to include any subclassed contributions
                                 )
                     )

class ASubclassed(ABase):
    a_sub_item = tr.Str('a_sub_item')
    custom_grp = trui.Group('a_sub_item')
    
class BBase(tr.HasTraits):
    b_base_item = tr.Instance(ABase)
    
    base_grp = trui.Group(trui.Item('b_base_item',style='custom'))
    view = trui.View(trui.VGroup(trui.Include('base_grp'),
                                 trui.Include('custom_grp') # easy way to include any subclassed contributions
                                 )
                     )

class BSubclassed(BBase):
    b_sub_item = tr.Str('b_sub_item')    
    custom_grp = trui.Group('b_sub_item')

inst = BSubclassed()
inst.b_base_item = ASubclassed()

inst.configure_traits()


I'd like to get it to be more like the following where there is very little in the model that has View/Controller references.

# no idea on trait naming here so I'm just using object and handler for clarity
class ABaseHandler(tr.Handler):
    btn = tr.Button('Action')
    base_grp = trui.Group('object.a_base_item') 
    handler_grp = trui.Group('handler.btn')
    view = trui.View(trui.VGroup(trui.Include('base_grp'),
                                 trui.Include('object.custom_grp'), # if object is subclassed, it can contribute via a custom group
                                 trui.Include('handler_grp'),
                                 )
                     )

class ABase(tr.HasTraits):
    a_base_item = tr.Str('a_base_item')    
    default_handler = ABaseHandler() #This doesn't work but would be nice if it did

Thanks for any help and guidance you can provide,
Eric

The only other advice I can give is: Enaml has a much cleaner model-view separation, but I don't know if that is technologically feasible for you; and if you are on top of Qt, then you may find PyFace's Tasks to be useful, as it is designed to allow parts of the UI to contribute functionality to menubars and toolbars.

The other design patter we use frequently is to have a Model HasTraits class which is pure Traits, no TraitsUI; and then have a second HasTraits class which holds a reference to the Model HasTraits, and builds a view for it.  Sometimes this second class will be a ModelView handler, sometimes not, depending on circumstances.

And then, getting back to the original comment, rather than bending over backwards to make TraitsUI behave in a way that is not natural for it, convince your colleagues that the standard way of doing things is providing a "default view" which any code can override with its own view for the object if needed.

-- Corran 


 


_______________________________________________
Enthought-Dev mailing list
[hidden email]
https://mail.enthought.com/mailman/listinfo/enthought-dev



_______________________________________________
Enthought-Dev mailing list
[hidden email]
https://mail.enthought.com/mailman/listinfo/enthought-dev
Reply | Threaded
Open this post in threaded view
|

Re: Separating Model from View/Controller

ericjmcd
In reply to this post by ericjmcd
I figured out a way to do what I wanted.  It's a bit hackish and might have some issues at some point since I overrode trait_view but it works for the below example.  Figured I'd post it in case someone else is looking for a similar paradigm.

There are three main things going on here:
1) The basic idea is to have a Base class that has a _view_sel trait that contains the 'desired' view type (where view type is generally descriptive: simple, advanced, compact, tree, etc.) that classes can choose to implement. Whether or not a particular node in the class tree implements it, it passes the request down through the _view_sel trait.  (As far as I can tell, there's no built-in way of passing this down from one view to the next except through explicit calls in the editor instantiation for an Item or in editor_args).
2) For a strict (well almost strict) separation of the Model and View/Controller, we insist on having handlers for all class nodes (or at least all that need specific Views and or extra handler traits/buttons/etc.). The handler creates multiple views to follow the above paradigm if desired.
3) A node in the class tree can be subclassed. The subclass can add new traits (and the need for new view_elements). Any subclassed node must also have the appropriate subclassed handler that can add the new traits to the views. This is accomplished by calling the view_factory of the original handler and then put the view's content (view.content) into a new view that adds the extra items.  This way, additions to the original class and/or the original handler will automatically show up in any subclassed instances.

import traits.api as tr
import traitsui.api as trui

class Base(tr.HasTraits):
    _view_sel = tr.Str('')
    _view_factory = tr.Callable
   
    # Override trait_view to pass on the current 'view selection' to any
    # submodules of type Base. If trait_view called without 'name' (arg[0])
    # or 'view element' (arg[1]) then just pass call to HasTraits (super)
    def trait_view(self, *args, **kws):
        # TODO - do we really need to check args vs. kws? I've never
        # seen the args show up in **kws.
        if (isinstance(args[0], trui.View) or len(args)>1
            or kws.has_key('view_element')):
            return super(Base, self).trait_view(*args, **kws)
       
        if kws.has_key('name'):
            name = kws['name']
        else:
            name = args[0]
       
        # If name isn't defined, use our local selection. If local
        # (i.e. _view_sel) is also undefined, it'll fall through to
        # super at the end
        if name == '':
            name = self._view_sel
       
        # Set the view for any submodules derived from Base
        for trait_name in self.editable_traits():
            trait = getattr(self, trait_name)
            if isinstance(trait, Base):
                trait._view_sel=name
        if self._view_factory:
            view = self._view_factory(name) # returns None if no match for name
            if view:
                return view
        # If we don't have a 'name' view in our factories, call the default
        return super(Base, self).trait_view()

class CHandler(trui.Handler):
    ch_btn = tr.Button('CH Button')
    @classmethod
    def view_factory(cls, view_name):
        if view_name=='simple':
            return trui.View(trui.Label('Simple'),
                             trui.Item('handler.ch_btn', show_label=False),
                             trui.Item('c_item'),
                             handler=CHandler())
        elif view_name=='advanced':
            return trui.View(trui.Label('Advanced'),
                             trui.Item('handler.ch_btn', show_label=False),
                             trui.Item('c_item', label='Simple View'),
                             handler=CHandler())
        return None
           

class CSubHandler(CHandler):
    csubh_btn = tr.Button('CSubH Button')
    @classmethod
    def view_factory(cls, view_name):
        view = super(CSubHandler, cls).view_factory(view_name)
        if view:
            if view_name == 'simple':
                return trui.View(view.content,
                                 trui.Label('CSubH Added Simple Content'),
                                 'handler.csubh_btn',
                                 handler=CSubHandler())
            elif view_name == 'advanced':
                return trui.View(view.content,
                                 trui.Label('CSubH Added Advanced Content'),
                                 'handler.csubh_btn',
                                 handler=CSubHandler())
            # Technically, we could have a 'default' view for the extra
            # content in the SubHandler but that's so rare don't bother,
            # plus, it's not straightforward how to do that on a class method
        return view
       
class BHandler(trui.Handler):
    @classmethod
    def view_factory(cls, view_name):
        if view_name=='simple':
            return trui.View(trui.Label('Simple'),
                             trui.UCustom('c'),
                             trui.UCustom('csub'),
                             handler=BHandler())
        elif view_name=='advanced':
            return trui.View(trui.Label('Advanced'),
                             trui.Group(trui.UCustom('c'),
                                        trui.UCustom('csub'),
                                        layout='tabbed'),
                             handler=BHandler())

   
class AHandler(trui.Handler):
    @classmethod
    def view_factory(cls, view_name):
        # Note for this one, we don't care what the view_name is,
        # everyone gets the same
        return trui.View(trui.Label('Same For All Views'),
                         trui.UCustom('b'),
                         handler=AHandler())
       

class C(Base):
    c_item = tr.Str('c_item')
    _view_factory = CHandler.view_factory

class CSub(C):
    csub_item = tr.Str('csub_item')
    _view_factory = CSubHandler.view_factory

class B(Base):
    c = tr.Instance(C)
    csub = tr.Instance(CSub)  
    _view_factory = BHandler.view_factory

class A(Base):
    b = tr.Instance(B)
    _view_factory = AHandler.view_factory
       
a=A(b=B(c=C(), csub=CSub()))
a.configure_traits(view='advanced')
a.configure_traits(view='simple')