Announcement Announcement Module
Collapse
No announcement yet.
Avoiding UI-centric Domain Classes Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • Avoiding UI-centric Domain Classes

    I have a question about what others are doing to avoid UI-centric features creeping into their domain classes. First a little setup.

    I am really digging the way this framework allows easy use of domain classes in UI layers. The one area I have struggled with architecturally is when UI-centric stuff needs to be added to support something like multiple selection lists or date ranges. I'd like my domain classes to be more logically pure. When faced with this in the past few apps, I used to simply slam this support directly into the domain but this seems wrong. Now what I've started doing is to create a UI-centric decorator for a domain class that needs additional UI 'features'. An example might help explain what I'm getting at...

    Assume we have a domain Widget
    Code:
    package abc.domain;
    public class Widget
    {
      private String name = null;
      private List cogs = null;
    
      public Widget()
      {
      }
    
      public String getName()
      {
        return name;
      }
      public List getCogs()
      {
        if (cogs == null) {
          cogs = new ArrayList();
        }
        return cogs;
      }
    ...
    }
    Because I know I'm going to extend this (below), I go ahead and refactor my domain class by creating an interface and renaming the concrete class as shown:
    Code:
    package abc.domain;
    public interface Widget
    {
      String getName();
      void setName(String val);
      List getCogs();
      void setCogs(List val);
    }
    
    public class WidgetImpl implements Widget
    {
      ...
    }
    I need to now provide a widget edit controller that allows 0 to many cogs to be added to a widget via a multiple selection list. A common way to do this is to provide a referenceData handler to populate a list of selections (e.g. "selectedCogs")The list contains SelectionOption instances (a pojo class that has name, value and a boolean indicating if it is selected) and is built by adding all available cogs to be selected and then marking those as selected if they are found in the widget.cogs list.

    The view (jsp in this case) uses this reference list to build the select/option dynamically. The select is bound to a new ui-centric form attribute called something like cogIds which is a String. When the form binds (during a POST), any option that is selected is passed to the cogIds as a comma delimited list.

    To make this work, I decorate/extend Widget like this
    Code:
    package abc.ui.forms;
    import abc.domain.Widget;
    
    public class WidgetForm implements Widget
    {
      private Widget delegate = null;
    
      public WidgetForm(Widget val)
      {
        super();
        delegate = val;
      }
    
      public String getName()
      {
        return delegate.getName();
      }
      ...
      // UI methods
      public void setCogIds(String val)
      {
        delegate.setCogs(functionToSplitStringIdsIntoCogList(val));
      }
             
    }
    The jsp code fragment might look like
    Code:
    <spring&#58;bind path="command.cogIds">
    <label><fmt&#58;message key="label.$&#123;status.expression&#125;" /></label>
    <select multiple name="$&#123;status.expression&#125;">
    <c&#58;forEach items="$&#123;assignedCogs&#125;" var="selectionOption">
      <option <c&#58;if test="$&#123;selectionOption.selected&#125;"> selected </c&#58;if> value="$&#123;selectionOption.value&#125;">
      $&#123;selectionOption.name&#125;
      </option>
    </c&#58;forEach>
    </select>
    <br/>
    </spring&#58;bind>
    The controller now simply provides a WidgetForm instance. The DomainForm is responsible for translating the UI-centric info into Domain-centric info.

    The net result is my domain's retain some logic purity (which is a bonus if I provide access to them via remoting for example) but it is somewhat messy to wrap a complex domain class inside a ui form. I'm also not a fan of domain interfaces in general. This also works well with input parameters to iBatis sql maps (they can be told the input is the interface) which allows them to work from a pure domain context but be provided UI-centric extensions.

    While I'm currently doing this via brute-force (my decorated ui form class delegates all calls manually) but am thinking of building a base class that will provide auto-magic reflection based decoration/delegation. Does this seem like a reasonable approach? Other thougts?

    BTW, I know it would be simpler to provide a ui form that simply has the domain as an attribute rather than extending it but I didn't want to the UI views to be aware of the extension.
    Code:
    $&#123;command.widget.name&#125;
    and
    $&#123;command.cogIds&#125;

  • #2
    I might not understand something but if you use command Controllers would not they perform UI functions for you, like splitting ids into list.

    Comment


    • #3
      You need to use PropertyEditors, so your form would have a setCogs(Cog[] cogs) method.

      Then create a PropertyEditor that converts to and from a Cog[] to a String[] of ids. Register this propertyEditor for the Cog[] class in initDataBinder.

      Then in your jsp you will get a Cog[] when binding to command.cogs.

      Hope this helps.

      Note: If you decide to specify a Set of cogs, then you can do this, but obviously don't register your propertyEditor against a Set.class Register it against the field name.

      Comment


      • #4
        Maybe I'm being dense... I'm having some trouble setting up what yatesco suggests. I don't have Cogs, but I do have SelectableMarkers. Anyhow - I have an entity class - a QcRequest. A QcRequest has a list of child SelectableMarker's. I'm using FreeMarker (love it, btw - I hope to never touch a JSP again, ever) and have my wonderful little property editor. Seems, though, that the property editor is converting the list to a string (as in editor.getAsText()) and is driving the checkbox macro nuts. Here's the relavent code and such:

        Code:
        public class QcRequest &#123;
          private int pk;
          ...
          private List<SelectableMarker> bacterialSelectableMarkers;
        
          ... various getters/setters and few methods that 'do stuff'
        &#125;
        
        public class SelectableMarker &#123;
          private int pk;
          private String marker;
          private String abbreviation;
        
          ... again with the getter/setters
        &#125;
        And a property editor:

        Code:
        public class SelectableMarkerListEditor extends PropertyEditorSupport &#123;
          private Map<String, SelectableMarker> selectableMarkerMap;
        
          public SelectableMarkerListEditor&#40;List<SelectableMarker> markerList&#41; &#123;
            this.selectableMarkerMap = new HashMap<String, SelectableMarker>&#40;markerList.size&#40;&#41;&#41;;
            for &#40;SelectableMarker marker &#58; markerList&#41; &#123;
              this.selectableMarkerMap.put&#40;Integer.toString&#40;marker.getPk&#40;&#41;&#41;, marker&#41;;
            &#125;
          &#125;
        
        
          public void setAsText&#40;String text&#41; &#123;
            String&#91;&#93; keys = text.split&#40;","&#41;;
            List<SelectableMarker> markers = new ArrayList<SelectableMarker>&#40;keys.length&#41;;
        
            for &#40;int i=0; i<keys.length; ++i&#41; &#123;
              markers.add&#40;selectableMarkerMap.get&#40;keys&#91;i&#93;.trim&#40;&#41;&#41;&#41;;
            &#125;
        
            setValue&#40;markers&#41;;
          &#125;
        &#125;
        The relevant bit of the freemarker template that renders the control (er, blows up spectacularly - more on that below):

        Code:
              <tr>
                <td><@genie.label "form.label.selectableMarkers"/></td>
                <td><@spring.formCheckboxes path="request.bacterialSelectableMarkers"
                                            options=selectableMarkers
                                            separator="<br/>"/>
                     <@spring.showErrors separator="<br/>" classOrStyle="error"/>
                </td>
              </tr>
        The code that populates the view model with the list o' available markers:

        Code:
        ... from the relevant referenceData&#40;&#41; override. &#40;Spring MVC - good times&#41;
        
            Map<String, Object> model = new HashMap<String, Object>&#40;&#41;;
            model.put&#40;"selectableMarkers", viewListService.getSelectableMarkerMap&#40;&#41;&#41;;
            return model;
        
        ... the interface for the viewListService&#58;
        
        public interface ViewListService &#123;
          public Map<String, String> getVectorTypeMap&#40;&#41;;
          public Map<String, String> getSelectableMarkerMap&#40;&#41;;
        &#125;
        and, of course, registering the property editor:

        Code:
          @Override protected void initBinder&#40;HttpServletRequest request, ServletRequestDataBinder binder&#41; &#123;
            binder.registerCustomEditor&#40;List.class, "bacterialSelectableMarkers",  new SelectableMarkerListEditor&#40;selectableMarkerService.getMarkers&#40;&#41;&#41;&#41;;
          &#125;
        Ok, so that all looks fine to me. Problem is, it blows up. Not so good. Here's the lovely stack trace:

        Code:
        freemarker.template.TemplateException&#58; Expected collection or sequence. list evaluated instead to freemarker.template.SimpleScalar on line 300, column 16 in spring.ftl.
        	freemarker.core.TemplateObject.invalidTypeException&#40;TemplateObject.java&#58;135&#41;
        	freemarker.core.IteratorBlock$Context.runLoop&#40;IteratorBlock.java&#58;183&#41;
        	freemarker.core.Environment.visit&#40;Environment.java&#58;351&#41;
        	freemarker.core.IteratorBlock.accept&#40;IteratorBlock.java&#58;95&#41;
        	freemarker.core.Environment.visit&#40;Environment.java&#58;196&#41;
        	freemarker.core.MixedContent.accept&#40;MixedContent.java&#58;92&#41;
        	freemarker.core.Environment.visit&#40;Environment.java&#58;196&#41;
        	freemarker.core.Macro$Context.runMacro&#40;Macro.java&#58;164&#41;
        	freemarker.core.Environment.visit&#40;Environment.java&#58;537&#41;
        	freemarker.core.MethodCall._getAsTemplateModel&#40;MethodCall.java&#58;105&#41;
        	freemarker.core.Expression.getAsTemplateModel&#40;Expression.java&#58;89&#41;
        	freemarker.core.Assignment.accept&#40;Assignment.java&#58;91&#41;
        	freemarker.core.Environment.visit&#40;Environment.java&#58;196&#41;
        	freemarker.core.MixedContent.accept&#40;MixedContent.java&#58;92&#41;
        	freemarker.core.Environment.visit&#40;Environment.java&#58;196&#41;
        	freemarker.core.IteratorBlock$Context.runLoop&#40;IteratorBlock.java&#58;160&#41;
        	freemarker.core.Environment.visit&#40;Environment.java&#58;351&#41;
        	freemarker.core.IteratorBlock.accept&#40;IteratorBlock.java&#58;95&#41;
        	freemarker.core.Environment.visit&#40;Environment.java&#58;196&#41;
        	freemarker.core.MixedContent.accept&#40;MixedContent.java&#58;92&#41;
        	freemarker.core.Environment.visit&#40;Environment.java&#58;196&#41;
        	freemarker.core.Macro$Context.runMacro&#40;Macro.java&#58;164&#41;
        	freemarker.core.Environment.visit&#40;Environment.java&#58;537&#41;
        	freemarker.core.UnifiedCall.accept&#40;UnifiedCall.java&#58;128&#41;
        	freemarker.core.Environment.visit&#40;Environment.java&#58;196&#41;
        	freemarker.core.MixedContent.accept&#40;MixedContent.java&#58;92&#41;
        	freemarker.core.Environment.visit&#40;Environment.java&#58;196&#41;
        	freemarker.core.Environment.process&#40;Environment.java&#58;176&#41;
        	freemarker.template.Template.process&#40;Template.java&#58;231&#41;
        	org.springframework.web.servlet.view.freemarker.FreeMarkerView.processTemplate&#40;FreeMarkerView.java&#58;267&#41;
        	org.springframework.web.servlet.view.freemarker.FreeMarkerView.doRender&#40;FreeMarkerView.java&#58;221&#41;
        	org.springframework.web.servlet.view.freemarker.FreeMarkerView.renderMergedTemplateModel&#40;FreeMarkerView.java&#58;180&#41;
        	org.springframework.web.servlet.view.AbstractTemplateView.renderMergedOutputModel&#40;AbstractTemplateView.java&#58;160&#41;
        	org.springframework.web.servlet.view.AbstractView.render&#40;AbstractView.java&#58;250&#41;
        	org.springframework.web.servlet.DispatcherServlet.render&#40;DispatcherServlet.java&#58;928&#41;
        	org.springframework.web.servlet.DispatcherServlet.doDispatch&#40;DispatcherServlet.java&#58;705&#41;
        	org.springframework.web.servlet.DispatcherServlet.doService&#40;DispatcherServlet.java&#58;625&#41;
        	org.springframework.web.servlet.FrameworkServlet.serviceWrapper&#40;FrameworkServlet.java&#58;386&#41;
        	org.springframework.web.servlet.FrameworkServlet.doPost&#40;FrameworkServlet.java&#58;355&#41;
        	javax.servlet.http.HttpServlet.service&#40;HttpServlet.java&#58;709&#41;
        	javax.servlet.http.HttpServlet.service&#40;HttpServlet.java&#58;802&#41;
        	net.sf.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter&#40;FilterChainProxy.java&#58;292&#41;
        	net.sf.acegisecurity.intercept.web.FilterSecurityInterceptor.invoke&#40;FilterSecurityInterceptor.java&#58;84&#41;
        	net.sf.acegisecurity.intercept.web.SecurityEnforcementFilter.doFilter&#40;SecurityEnforcementFilter.java&#58;182&#41;
        	net.sf.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter&#40;FilterChainProxy.java&#58;303&#41;
        	net.sf.acegisecurity.providers.anonymous.AnonymousProcessingFilter.doFilter&#40;AnonymousProcessingFilter.java&#58;154&#41;
        	net.sf.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter&#40;FilterChainProxy.java&#58;303&#41;
        	net.sf.acegisecurity.ui.AbstractProcessingFilter.doFilter&#40;AbstractProcessingFilter.java&#58;311&#41;
        	net.sf.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter&#40;FilterChainProxy.java&#58;303&#41;
        	net.sf.acegisecurity.context.HttpSessionContextIntegrationFilter.doFilter&#40;HttpSessionContextIntegrationFilter.java&#58;215&#41;
        	net.sf.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter&#40;FilterChainProxy.java&#58;303&#41;
        	net.sf.acegisecurity.securechannel.ChannelProcessingFilter.doFilter&#40;ChannelProcessingFilter.java&#58;168&#41;
        	net.sf.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter&#40;FilterChainProxy.java&#58;303&#41;
        	net.sf.acegisecurity.util.FilterChainProxy.doFilter&#40;FilterChainProxy.java&#58;173&#41;
        	net.sf.acegisecurity.util.FilterToBeanProxy.doFilter&#40;FilterToBeanProxy.java&#58;125&#41;
        which just says that this line here is blowing up: (if you don't know freemarker macro syntax, don't worry -- it's all pretty easy)

        Code:
        <#function contains list item>
        	<#list list as nextInList>                         <--- line 300
        	<#if nextInList == item><#return true></#if>
        	</#list>
        	<#return false>
        </#function>
        which is being called by this:

        Code:
        <#macro formCheckboxes path options separator attributes="">
        	<@bind path/>
        	<#list options?keys as value>
        	<#assign isSelected = contains&#40;spring.status.value?default&#40;&#91;""&#93;&#41;, value&#41;>                       <---- which is being called here. 
        	<input type="checkbox" name="$&#123;spring.status.expression&#125;" value="$&#123;value&#125;"           ---- 'spring.status.value?default&#40;&#91;""&#93;&#41;' is a scalar?!
        		<#if isSelected>checked="checked"</#if> $&#123;attributes&#125;                                        
        	<@closeTag/> 
        	$&#123;options&#91;value&#93;&#125;$&#123;separator&#125;
        	</#list>
        </#macro>
        So, basically spring.status.value (ie, the current binding's value) is coming up as a scalar instead of as a sequence or list. In fact, if I comment out the call to the checkbox macro, call bind directly and print out the value of ${spring.status.value} - I get a lovely string-representation of my list. Which makes me think that the property editor is being applied to the property of the bean (bacterialSelectableMarkers) and the string value that the property editor is returning is being used by spring.status.value instead of the list value of the property. Which makes the macro bomb when it tries to apply a list function to it.

        Am I crazy, or did I forget to do something?

        Any help would be greatly appreciated......

        Comment


        • #5
          The problem is that the property editor shouldn't convert from a List to a string and back, it should convert from a single item (Cog or SelectableMarker).

          Spring will handle the "array" bit by choosing the appropriate (i.e. your) property editor to handle each element of the array. You register your PE against the class (SelectableMarker) and Spring will handle the array, not the List.class as Spring knows how to handle Lists or arrays etc.

          In your jsp/template if you want to display a list of selectableMarkers then you just bind on your property ${form.bacterialSelectableMarkers}.

          If you want to display all markers and preselect the chosen ones you will need to stick all the chosen ones in a map (have your form return a map as well) and during iteration, check the map.

          Hope this helps.

          Comment


          • #6
            It did. Sorry for not saying so months earlier. I was just noticing some of my ealier posts.

            Comment

            Working...
            X