Thursday, February 25, 2010

Pimping EEF: Master Detail

I've been spending quite a bit of time lately playing with the EEF framework. C'est magnifique! (No, I don't actually know any french, but I'm pretending in honor of the developers.)

Out of the box, I was amazed how quickly I was able to get up and running. A few glitches but nothing too traumatic. One biggie: for now you want to be using the M2 drop of Acceleo, not the current M5. But of course, nothing is exactly as you want it out of the box. So I've spent a bit of time figuring out how to customize things. I'm building a custom AMF editor as part of the Metascape toolset ad I wanted to make it as easy as possible for people to edit a model. The number one thing I need is Masters Detail support, so that people can edit item details without going to a separate view. No matter how nicely the environment is setup, I'm realizing that an integrated form is aways going to be easier for non-technical users to work with.

There quite understandably is not a lot of documentation available for EEF at present, so I had to do a bit of experimentation and solicit some very helpful advice from Goulwen and the rest of the EEF team. As my way of saying thank you, I dropped a bunch bug reports at their door. (But don't get me wrong, the basic setup is very usable and useful right now.)

So here's the basic recipe. To make it more general, I'm going to be referring to the EMF library example, but I actually implemented it in my own custom code so you probably won't be able to simply cut and paste in order to get this to work. The idea is that you're editing a library, and you want to be able to pick out an individual book and edit its details. I got it basically working a couple of days ago, but it has been a fair amount more work to get it working so that I can re-generate code without having to update everything -- always the biggest challenge with writing or using code-generation frameworks. It's also interesting that in the transition from the original changes that I reported in the forum post, I was able to chop out a bunch of unnecessary code, so the exercise of making the code work with re-generation also taught me a lot about how the overall framework works.



  1. In Repository for Library, add a Custom Element editor for "bookDetail".

  2. In Properties Edition for "Library", added a "bookDetail" that uses that view. I set the model to "books" because it needs to be bound to something, I think.

  3. Generate the code.
  4. Modified the "Library" Form. Here, we need to keep track of the subform and the book detail domain object:

    public class LibraryPropertiesEditionPartForm extends CompositePropertiesEditionPart implements IFormPropertiesEditionPart, LibraryPropertiesEditionPart {
    // Start of user code for bookDetail widgets declarations
    BookPropertiesEditionPartForm booksForm;

    BookPropertiesEditionComponent booksComponent;
    // End of user code
    ....


  5. In the same class, we want to add the form into the appropriate place, and add a selection listener. There are two important issues here. First, there is some ugly code because we need to hack into the component tree to find the bits we want to change the layout for. For now at least (I've got a bug request in!) we need to do this in the createControls because there is no us code section in the generated createBooksGroup code. Second, we need to locate the appropriate matching eObject using the generated utility. I wasn't clear that there was another object created, so it took a bit of time to figure this out.



    public void createControls(final FormToolkit widgetFactory, Composite view) {
    this.messageManager = messageManager;
    createDetailsGroup(widgetFactory, view);

    createBooksGroup(widgetFactory, view);

    createMembersGroup(widgetFactory, view);
    // Start of user code for bookDetail widgets implementation
    Section booksSection = (Section) view.getChildren()[1];
    Composite booksGroup = (Composite) booksSection.getChildren()[2];
    GridData booksGroupData = new GridData(SWT.LEFT, SWT.TOP, false, true);
    books.setLayoutData(booksGroupData);
    booksForm = new BookPropertiesEditionPartForm(propertiesEditionComponent);
    GridData booksGroupData2 = new GridData(SWT.FILL, SWT.TOP, true, false);
    booksForm.createControls(widgetFactory, booksGroup);
    booksGroup.getChildren()[1].setLayoutData(booksGroupData2);
    books.addSelectionListener(new SelectionListener() {

    public void widgetSelected(SelectionEvent e) {
    if (e.item != null) {
    Object data = e.item.getData();
    if (data instanceof Book) {
    if (e.item != null && e.item.getData() instanceof EObject) {
    EObject attr = booksEditUtil.foundCorrespondingEObject((EObject) e.item.getData());
    setCurrentBook((Book) attr);
    return;
    }
    }
    }
    //this probably isn't the right thing to do here!
    setCurrentBook(null);
    }

    public void widgetDefaultSelected(SelectionEvent e) {
    }
    });
    // End of user code


  6. The call to setCurrentBook looks like this:


    // Start of user code additional methods

    private void setCurrentBook(Book book) {
    LibraryCustomPropertiesEditionComponent libraryComp = (LibraryCustomPropertiesEditionComponent) propertiesEditionComponent;
    libraryComp.setBook(book);

    booksComponent = new BookPropertiesEditionComponent(book,
    IPropertiesEditionComponent.LIVE_MODE);
    booksComponent.setLiveEditingDomain(libraryComp.getLiveDomain());
    libraryComp.setBookProps(booksComponent);
    booksComponent.setPropertiesEditionPart(LibExampleViewsRepository.Book.class, 0, booksForm);
    booksComponent.addListener(booksForm);
    booksComponent.addListener(this);
    if (book != null) {
    booksComponent.initPart(LibExampleViewsRepository.Book.class, 0, book, resourceSet);
    } else {
    booksComponent.initPart(LibExampleViewsRepository.Book.class, 0, null, resourceSet);
    }
    }
    // End of user code

    As I said in my forum post, I'm not sure that this is the best way to do this -- there might be someway to simply fire off a property change and have a lot of this happen, but I wasn't able to figure that one out.



  7. Next, we need to create a custom property component for the Library. I started out trying to modify the generated code, but it turned out to be much easier and more maintainable to subclass it. Like this:

    public class LibraryCustomPropertiesEditionComponent extends LibraryPropertiesEditionComponent {

    private Book book;

    private BookPropertiesEditionComponent bookProps;

    public LibraryCustomPropertiesEditionComponent(EObject library, String editingMode) {
    super(library, editingMode);
    }

    /**
    * @param event
    * @see org.libexample.components.LibraryPropertiesEditionComponent#firePropertiesChanged(org.eclipse.emf.eef.runtime.api.notify.IPropertiesEditionEvent)
    */
    public void firePropertiesChanged(IPropertiesEditionEvent event) {
    super.firePropertiesChanged(event);
    if (getBook() != null && event.getAffectedEditor().startsWith("libexample::Book")) {
    getBookProps().firePropertiesChanged(event);
    }
    }

    /**
    * @param msg
    * @see org.libexample.components.LibraryPropertiesEditionComponent#runUpdateRunnable(org.eclipse.emf.common.notify.Notification)
    */
    protected void runUpdateRunnable(Notification msg) {
    if (msg.getNotifier() instanceof Library) {
    super.runUpdateRunnable(msg);
    } else if (msg.getNotifier() == getBook()) {
    getBookProps().runUpdateRunnable(msg);
    Library library = (Library) getBook().getOwner();
    basePart.updateBooks(library);
    }
    }

    /**
    * @param editingDomain
    * @return
    * @see org.libexample.components.LibraryPropertiesEditionComponent#getPropertiesEditionCommand(org.eclipse.emf.edit.domain.EditingDomain)
    */
    public CompoundCommand getPropertiesEditionCommand(EditingDomain editingDomain) {
    CompoundCommand propertiesEditionCommand = super.getPropertiesEditionCommand(editingDomain);
    if (book != null) {
    CompoundCommand attrEditionCommand = bookProps.getPropertiesEditionCommand(editingDomain);
    propertiesEditionCommand.append(attrEditionCommand);
    }
    return propertiesEditionCommand;
    }

    /**
    * @param source
    * @return
    * @see org.libexample.components.LibraryPropertiesEditionComponent#getPropertiesEditionObject(org.eclipse.emf.ecore.EObject)
    */
    public EObject getPropertiesEditionObject(EObject source) {
    EObject propertiesEditionObject = super.getPropertiesEditionObject(source);
    if (getBook() != null) {
    bookProps.getPropertiesEditionObject(getBook());
    }
    return propertiesEditionObject;
    }

    /**
    * @return the book
    */
    public Book getBook() {
    return book;
    }

    /**
    * @param book the book to set
    */
    public void setBook(Book book) {
    this.book = book;
    }

    /**
    * @return the bookProps
    */
    public BookPropertiesEditionComponent getBookProps() {
    return bookProps;
    }

    /**
    * @param bookProps the bookProps to set
    */
    public void setBookProps(BookPropertiesEditionComponent bookProps) {
    this.bookProps = bookProps;
    }

    //(We need to override this because its protected in the subclass.
    public EditingDomain getLiveDomain() {
    return liveEditingDomain;
    }
    }


    The idea is to use a delegate to the Books component, so that we don't have to copy a bunch of code around. Plus any changes we make to the Book forms in the model will automatically be used for the detail as well! The patterns are pretty simple once you break out what is happening in each method.

  8. Finally, we need to hook the whole thing up. Here, we take advantage of the adapter design to substitute our custom provider for the generated one. So first we need to create a custom provider for the Library, like so:

    public class LibraryCustomPropertiesEditionProvider extends LibraryPropertiesEditionProvider {

    /**
    * {@inheritDoc}
    *
    * @see org.eclipse.emf.eef.runtime.api.providers.IPropertiesEditionProvider#getPropertiesEditionComponent(org.eclipse.emf.ecore.EObject,
    * java.lang.String)
    */
    public IPropertiesEditionComponent getPropertiesEditionComponent(EObject eObject, String editing_mode) {
    if (eObject instanceof Library) {
    return new LibraryCustomPropertiesEditionComponent(eObject, editing_mode);
    }
    return null;
    }

    /**
    * {@inheritDoc}
    *
    * @see org.eclipse.emf.eef.runtime.api.providers.IPropertiesEditionProvider#getPropertiesEditionComponent(org.eclipse.emf.ecore.EObject,
    * java.lang.String, java.lang.String)
    */
    public IPropertiesEditionComponent getPropertiesEditionComponent(EObject eObject, String editing_mode, String part) {
    if (eObject instanceof Library) {
    if (LibraryPropertiesEditionComponent.BASE_PART.equals(part))
    return new LibraryCustomPropertiesEditionComponent(eObject, editing_mode);
    }
    return null;
    }
    }



  9. Then, we have to override the default package provider so that it provides the custom provider that we've just defined above. Is it just me, or is all of this Providers for Providers from Adapters stuff super confusing to you too? It's definitely worth the confusion to have the the power you get out of it in the end, but I find this stuff really difficult to wrap my head around until I've been working with a particular implementation for quite a while.

    public class LibExampleCustomPackagePropertiesEditionProvider extends LibExamplePackagePropertiesEditionProvider {

    /**
    * This creates an PropertiesEditionProvider for a Library
    */
    public LibraryPropertiesEditionProvider createLibraryPropertiesEditionProvider() {
    if (libraryPropertiesEditionProvider == null)
    libraryPropertiesEditionProvider = new LibraryCustomPropertiesEditionProvider();
    return libraryPropertiesEditionProvider;
    }

    }



  10. OK, next we just have to change the plugin.xml so that we use the custom package provider instead of the auto-generated one. This is how:

    <extension
    point="org.eclipse.emf.eef.runtime.PropertiesEditionProvider">
    <PropertiesEditionComponentProvider
    providerClass="org.libexample.providers.LibExampleCustomPackagePropertiesEditionProvider">
    </PropertiesEditionComponentProvider>
    </extension>



And here's what it looks like for the custom AMF editor:



So, that's it! There are still some pieces missing, and it took me a few days to figure out, but now that I've got most of the sticking points out of the way it looks like it is going to be a big boost to my ability to design custom interfaces without the pain of hand assembling forms.Hopefully you'll be able to get something up and working in a few hours. Wether you have simple needs for ready built forms, or more complex designs like this one, EEF is definitely worth checking out.

6 comments:

  1. Miles,

    thanks to share your (deep) experiments with EEF !

    ReplyDelete
  2. Hi, the result is pretty cool. I wonder why you need to customize the code. Woudn't this be possible by created nested views in the components model? (I don't speak from experience, I just read it's possible to nest views).

    ReplyDelete
  3. Hi,

    I was thinking the same thing myself, but I explored quite a bit. The hard part was not the nested views, but the master detail connection. The information you need doesn't really exist in the EEF model -- or didn't at the time this was written. That said, there may very well be much more efficient ways of doing this that I overlooked.

    cheers,

    Miles

    ReplyDelete
  4. Hi Miles,

    Thank you for this great recipe! It works like a charm.

    Here is my 2 cents hint:
    To get rid of the nasty hard coded array access to retreive Section & Composite from view's children on your 5th point code example, I placed the custom element editor in the corresponding group view from the EEF component model. This way, the detail widget implementation user code is placed in the createXXXGroup method and you have directly access to the Section & Composite local variables.

    Cheers,
    Gonzague.

    ReplyDelete
  5. Great! I had to implement a custom semantic sensitive component in my editor, your blog post helped me a lot! Thx!

    @xavier_seignard

    ReplyDelete
  6. Glad to hear it. I was thinking that this might be getting out dated as EEF is including more support for this kind of thing now.

    ReplyDelete

Popular Posts

Recent Tweets

    follow me on Twitter