Link to home
Start Free TrialLog in
Avatar of mrwad99
mrwad99Flag for United Kingdom of Great Britain and Northern Ireland

asked on

Document for CTreeView data representation

Ah hello.

I have an app that is based on CTreeView.  I also have a CDocument derived class that contains the data for this view.  My data structure is as follows:

class CItem{}; // abstract

class CConcreteItem : public CItem {};

class CContainer : public CItem // abstract
{
protected:
  std::list<CItem*>m_pContents;
};

class CConcreteContainer : public CContainer {}

class CProject : public CContainer  {};

This has been stripped for ease of reading.  Basically, in my CDocument derived class, I start with a CProject*, and add other CItem derived instances to it.  This follows the composite design pattern.  So in my CTreeView, I could have

+ MyProject(a CProject)
  + MyFirstContainer(a CConcreteContainer)
     + MySecondContainer(another CConcreteContainer)
        + MyFirstItem(a CConcreteItem)
        + MySecondItem(another CConcreteItem)

Where MyProject contains MyFirstContainer, which contains MySecondContainer, which contains MyFirstItem and MySecondItem.

Easy to see (hopefully)

Ok then.  Now for the questions.

I have previously been updating my document the incorrect way.  What I was doing, when a user wanted to add, say CMyThirdItem to MySecondContainer, was to get hold of the data item associated with the tree node MySecondContainer via GetItemData(), call a method in my document to say "Create a new CConcreteItem instance called CMyThirdItem, and add it to CMySecondContainer" then inserting a new node into the tree control under MySecondContainer to represent this additon.  So the tree view would now look like

+ MyProject(a CProject)
  + MyFirstContainer(a CConcreteContainer)
     + MySecondContainer(another CConcreteContainer)
        + MyFirstItem(a CConcreteItem)
        + MySecondItem(another CConcreteItem)
        + MyThirdItem (another CConcreteItem)

However, what I believe I should be doing is to handle all of the adding of data in the CDocument class first, then calling UpdateAllViews() (passing appropriate parameters as the hint, to avoid a total redraw of the view) to tell the tree view to redraw itself.

Q1) Is this the best option in this case ?

If I did do this, then I came across the problem of not knowing where to insert the new item created into my data structure.  As mentioned before, I was obtaining this via GetItemData(), i.e.

CItem* pParentItem = (CItem*)GetItemData(currently_Selected_Tree_Control_Node)

// can now insert data of any new item to be added to "currently_Selected_Tree_Control_Node" into pParentItem

but if I had to do all the addition in the document, I could not do this.  I had an idea that what I could do is to call GetItemData() whenever the selection changes in the tree control, setting some member variable in the document class to be the item returned.  But this would mean that the document and the view would have to "know about each other" to some extent...I thought the whole idea of using UpdateAllViews() was to avoid that, as Jaime states at http:Q_21330318.html

Q2) What is the correct way to solve this problem ?

Many TIA
Avatar of rcarlan
rcarlan

Manually updating the active View in an MDI Doc-View application as a result of a user operation is generally not a very good design. The main reason is that you may have multiple views for one document. These views have to be kept in sync. Should you update the active view as a result of a command processed in the view class, you would leave the other views with stale (out of sync) data.

The best approach is to delegate the processing to the document object and then push updates to all views from the document. This is why there is an UpdateAllViews method in CDocument.

Of course, you do not want to fully redraw all views when you call UpdateAllViews. This would result in flicker and possibly a lot of processing - not a very pleasant user experience. That's why UpdateAllViews has provision for a 'hint'.

For your tree view, the best approach would probably be to define hint objects (atomic commands) that you can pass to the tree view to optimise the update routine. For example, you could have the following atomic view commands: AddNode, RemoveNode, MoveNode, RefreshNode. Each view command would have associated parameters. These parameters would refer to nodes in the tree (not using HTREEITEM, but some internal IDs - can even be CItem pointers, if that's what you store in the tree). It's important to understand that these are just commands for the views (i.e. the document's data is updated separately - in the document, and in the process the corresponding view commands would be collected in a list). When you call UpdateAllViews, you would pass the collection of view commands as a hint.

Each view would process the atomic commands - one at a time - and update itself accordingly.

Radu

Avatar of AndyAinscow
In my opinion the place for the data 'store' is the document.  You can then (as you say) use hints and UpdateAllViews to let the views requery the document for data and redraw themselves.

Where to store the new object in the data structure?
From what you write it sounds like you store the pointer to the parent object in the object itself.  If that is the case just iterate up the chain of parents until you reach NULL (top of tree).  Now you have pointers to each node in the ownership chain and you can add the child to the appropriate list at the correct insert point.  This does NOT require an intimate link between view and document.  
Avatar of mrwad99

ASKER

Hi Radu

Thanks for commenting on my question.  I understand what you are saying, but the question still remains as to how I know what element of the data structure to insert the new item into.  As mentioned above,

CItem* pParentItem = (CItem*)GetItemData(currently_Selected_Tree_Control_Node)

// can now insert data of any new item to be added to "currently_Selected_Tree_Control_Node" into pParentItem


I cannot do this if I delegate all the data processing into the document object, since I have not got access to the currently selected tree node so cannot get the data item associated with it, which is to be the parent of my new item!

Any ideas ?
Looking at your data structure itself it is not that complex.  
Each container contains a list of children.  All you need is to get the pointer to the container that has the new node (parent) and add the new node to its internal list (you can get the sibling nodes of the new item in the tree to 'inform' the container of where in the list to insert the new node).  Now tell the doc the data structure has been changed and let that update the views via the UpdateAllViews/hint mechanism.
You can also use the lParam member of the item you add to the tree for the pointer to the object in memory (should you not have a pointer to the parent as a member of the child item).

TVITEM
Specifies or receives attributes of a tree-view item. This structure is identical to the TV_ITEM structure, but it has been renamed to follow current naming conventions. New applications should use this structure.


typedef struct tagTVITEM{
    UINT      mask;
    HTREEITEM hItem;
    UINT      state;
    UINT      stateMask;
    LPTSTR    pszText;
    int       cchTextMax;
    int       iImage;
    int       iSelectedImage;
    int       cChildren;
    LPARAM    lParam;
} TVITEM, FAR *LPTVITEM;

Avatar of mrwad99

ASKER

Hi Andy

Thanks for joining in.

I am aware of everything that you have said, but I appreciate the effort nonetheless.

My problem, as mentioned, is the obtaining of the parent data structure to insert the new item into.  Again, if I have the tree structure

+ MyProject(linked to a CProject*)
  + MyFirstContainer(linked to a CConcreteContainer*)
     + MySecondContainer(linked to another CConcreteContainer*)
        + MyFirstItem(linked to a CConcreteItem*)
        + MySecondItem(linked to another CConcreteItem*)

and the user, say, wants to add another CConcreteItem to MySecondContainer.  The current implementation of my project would allow this operation via a right click on node "MySecondContainer" and selecting "Add new CConcreteItem" from a context menu.  

Now, updating the tree control on its own is easy; I simply add a new node as a child to the item that was right clicked on, i.e. CMySecondContainer. But updating my data structure is not so simple.

I know I have to create a new CConcreteItem*.  And I know that in order to keep an accurate data structure, this new item should be added to the data structure of the parent node, i.e. the CConcreteContainer* that is linked to MySecondContainer.  My question is

"How do I get hold of the CConcreteContainer* that is linked to MySecondContainer ?"

I have given a suggestion of:

every time a new node is selected in the tree view, obtain a pointer to the CItem* associated with it, and store this in some member variable in the CDocument.

EG

CMyTreeView::OnSelChanged(...)
{
   HTREEITEM hCurSel = <Currently selected item>
   CItem* pCurData = (CItem*)GetItemData(hCurSel);  // Get CItem associated with the current selected tree node
   GetDocument()->SetCurSel(pCurData);  // Set some member variable in document to be this pointer
}

Then in my CDocument class,

void CMyDoc::SetCurSel(CItem* pItem)
{
  m_pCurSel = pItem;
}

// Handler for adding a new ConcreteItem
afx_msg void OnNewConcreteItem()
{
  CConcreteItem* pNewItem = new CConcreteItem();
  m_pCurSel->Add(pNewItem);  // Add the new item into m_pCurSel, which is its parent

  // Mess around with UpdateAllViews() here to redraw the view, adding only this new element

}

But I was not sure if this is the best way of going about this, or if this is a hack, or whatever.  Please comment.

Thanks.
I think that is unnecessary.
In the tree one can fill the lParam member var of the TVITEM with the pointer to the object in memory.
When you add a new item USING THE TREE FUNCTIONS you can a) get the parent node - and hence the memory location of the parent of the new node and call that to add the new child and b) the siblings - so you now the pointer to the object directly before/after the insert point, which allows you to add the new item to the correct position in the list the parent has internally.
Note that involves no functions in the doc/view, all the location functions use the CTReeCtrl member functions, the inserting the new item use functions of your CItem/CContainer.  Once the insert has been done at that point you tell the doc that data has changed and that will then pump the views to redraw.
The SetCurSel in the document is not a good idea. You have to remember that there may be many views over the same document. Even if now you only have one, in the future somebody may decide that they want to support the New View command. With many views over the one document, each will have a different current selection.

Each tree view item needs to store the identity of the data item it corresponds to. Using a CItem pointer for this (as you do now) is acceptable, but you have to make sure all tree items corresponding to a CItem about to be deleted, are removed from the tree before the CItem pointer is invalidated. It is acceptable to rely on UpdateAllViews for this, too.

I would put methods CMyDoc like so:
void CreateChildItem(CItem* pParent);
void RemoveItem(CItem* pItemToRemove);
...
You may want to add parameters for some of these methods, if the view itself acquires some information from the user - e.g. CreateChildItem(CItem* pParent, LPCTSTR psName).

These methods update the internal data structures of the CMyDoc instance and then send notifications to the views. You have to make sure you do not delete CItem pointers in CMyDoc until after UpdateAllViews has returned. Each of these methods in CMyDoc would create notification objects (e.g. CItemCreated, CItemDeleted, ...). These notification objects would be sent to the views through the UpdateAllViews method. You can send more than one notification at a time (i.e. the hint is a collection).

The class diagram would look something like this:

          CObject
              |
CTreeViewUpdateHint  ----->(*)  ITreeViewNotification*
                                                            |
                                               -----------------------
                                               |           ...            |
          CItem* (2)<---   CItemCreated     ...      CItemDeleted --->(1)  CItem*
  (parent & new child)                                                                 (deleted item)


The ITreeViewNotification interface (abstract class) needs to have at least one method like so:
           virtual void ApplyToTreeView(CTreeView*) = 0;
You may decide there are other methods that are useful for your design (for example, you should consider having a virtual destructor in it with an empty implementation). Each derived class would provide its own implementation for the ApplyToTreeView method. These implementations may delegate back to the tree view (i.e. double dispatch).

Anyway, with this method in place, in the tree view's OnUpdate method (called by UpdateAllViews), you would dynamic_cast the hint object from CObject to CTreeViewUpdateHint, iterate over its ITreeViewNotification collection and call ApplyToTreeView(this) for each notification pointer in this collection.

The methods in CMyDoc would create ITreeViewNotification instances (i.e. derived objects - ITreeViewNotification is an interface), add them to a CTreeViewUpdateHint and pass the latter into UpdateAllViews.

Radu

Avatar of mrwad99

ASKER

Thanks for those comments again both.  Heh, since I am not at your level of understanding yet I am not fully competent to go off and do this all yet.  I will of course give more points for the extended explanations.  Firstly:

Andy

>> When you add a new item USING THE TREE FUNCTIONS you can a) get the parent node...

Yes I am aware of this, and that is the way I was previously doing it.  But this is not the way I am trying to... you are suggesting (it looks like)
1) adding the new node in the tree control
2) getting the data associated with the parent node
3) adding the data to be linked to the new node into the data found in 2)

>> Once the insert has been done at that point you tell the doc that data has changed and that will then pump the views to redraw.

What will be need to redrawn though ?  The new item has already been added into the tree control

?!??!


Radu

There is such a lot there that I will need to re-read it again and again.  I think you and Andy are getting at the same point, is that correct ?
What will be need to redrawn though ?  The new item has already been added into the tree control

The other views ?  If another view shows data related to the selected item in the tree AND the addition results in that item being selected then the view(s) needs to be invalidated.
I have an instance where the tree displays items in an invoice.  Adding a new item will modify the invoice total for example.
Avatar of mrwad99

ASKER

So you suggest that I have the handler for adding a new item *in my view* class ?

Is this the basic (pseudo) code you are getting at ?

afx_msg void CMyView::OnInsertItem()
{
   CConcreteItem* pItem = new CConcreteItem();  // Create a new item
   HTREEITEM hCurSel = GetTreeCtrl().GetCurSel(); // Get Currently selected tree node
   CConcreteContainer* pParent = (CConcreteContainer*)GetTreeCtrl().GetItemData(hCurSel);  // Get Parent data
   pParent->Add(pItem);  // assumes Add() exists to add an item to the std::list member
   GetDocument()->SetModified(TRUE);

  //....

At this point, I am not sure if I should

a) Add the new item into the tree control via GetTreeCtrl().InsertItem(...) and then associate pNewItem with it via GetTreeCtrl().SetItemData(), or

b) Jump to some method in the document class that then calls UpdateAllViews()...

You see Andy, I am getting confused because I am not sure if all of the adding of items should be done in the document or the view or where.  I know I have to add the new item into the document object somehow, which I can do by just adding the new item to its parent.


Radu, I would be *eternally* grateful if you could give me ten minutes of your time and construct a simple test app that demonstrates what you have said.  An example speaks a million words, and all that...

Thanks.
   
Where to add new items?
Can you add by context menu (right mouse click)?, toolbar button?, menu item?  
For me it makes sense that the reacting (adding) is just done at one piece of code and as these are user events then the frame/view makes more sense than the doc (frame/view for the display, doc for the storage).  What I then suggested is that the items themselves contain the actual code to handle adding the new item to the internal lists.  At this point the doc then needs to be informed that there has been a change in the data structures so that all views that require updating are done so.

Does that help?
Avatar of mrwad99

ASKER

Andy, I am not being lazy, but could you please give me a simple project outlining what you have suggested.  Otherwise I just going to go around in circles trying to clarify what you have said.  I have listed the classes above that comprise the data structure, all I ask of you is to stick those in a project and add what you have said.  Then I can print it off, and your comments will make all the more sense.

Thanks.
ASKER CERTIFIED SOLUTION
Avatar of rcarlan
rcarlan

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of mrwad99

ASKER

Radu

That is not boring.  It is hard however to understand fully since I cannot see it working.  I would appreciate it if you could please, as I asked above, put this into a simple project I can download run.  Then I can see these things in action, without having to struggle with where to put them, but instead focussing on how they work.

Description is excellent, but what is better is being able to play with it as a whole.  I learn by example you see.

Thanks.
I was hoping the code snippets above would be enough to get you going. You have to realise that writing even a skeleton demo application requires quite a bit of time. I whish I had a couple of hours to spare to do it, but unfortunately I am way behind in my work as it is. I'm sorry, but I cannot afford the time to do what you ask. I hope you will understand and not be offended.

I suggest you take the time to analyse the solution described above and code the prototype yourself. I'm happy to answer specific questions you may have during this process to make it easier for you to get it up and running.

Regards,
Radu
Avatar of mrwad99

ASKER

Hi Radu

Yeah that is fine, I fully appreciate what you have done so far.  I took your advice and had an in depth study of the code without putting anything to machine.  I have just one question as far as I can see, since I have digested what you have said and can see that it really is clever.

In Document class

>> void CreateChildItem(CItem* pParent);

That as you have stated requires the parent item to be passed in.  Obviously I can then say in the body:

{
  Create a new item
  add that new item to pParent
  Call UpdateAllViews(), passing one of your custom classes as described
}

Simple.  But the question is how that parent item gets passed; indeed how CreateChildItem gets called.  

The way I think I should do it is as follows.  This is what I would like you to comment on, please.

1) Suppose in the view class, a user right clicks a random tree node and chooses to add a new item to it from the context menu displayed.  Obviously the visual result will be a new child node whose parent is the node that was originally right clicked.  Suppose I have a handler for that menu item that was selected from the context menu:

afx_msg void OnAddNewItem()

I think I should then say

{
  HTREEITEM hCurSel = GetTreeCtrl().GetCurSel();
  CConcreteContainer* pParent = GetTreeCtrl().GetItemData(hCurSel);
  GetDocument()->CreateChildItem(pParent);
}

*Is that correct?*

Thanks for the excellent advice and persistence in helping me through this :)
That's exactly what you need to do in the view.
In the document you would construct a CItemCreated instance, which presumably would have to have at least two data members: existing parent item and new child item.
You send this as a hint to all views. Each tree view would locate the tree node corresponding to the parent item and add a tree node under it corresponding to the new child item.
Presto: decoupling between presentation layer and business logic.

Now you can have as many views as you want active, all will be in sync and none will have to process user input - other than to delegate to the document. The view becomes much thinner: collect user input (i.e. command), send it to the document, listen for update requests, refresh/update view based on the notification sent by the document.

You can also add other view types (not just tree views) and they can respond in their own way to the relevant notifications.
You can have any number of different notifications (item created, item deleted, item moved, item changed, etc).
You should consider making the hint a collection of notifications (i.e. do not send a single ITreeCommand, or whatever you want to call it). Place it in a container and send the container. This way you can send update notifications in batches. It’s possible that certain user actions result in multiple items being changed in the document, and you don’t want to call UpdateAllViews multiple times.

Flexible, scalable, decoupled.

See you around.
Radu
afx_msg void OnAddNewItem()

I think I should then say

{
  HTREEITEM hCurSel = GetTreeCtrl().GetCurSel();
  CConcreteContainer* pParent = GetTreeCtrl().GetItemData(hCurSel);
  GetDocument()->CreateChildItem(pParent);
}

*Is that correct?*


That is very similar to what I was suggesting.  I had thought of code like this.
{
  HTREEITEM hCurSel = GetTreeCtrl().GetCurSel();
  CConcreteContainer* pParent = GetTreeCtrl().GetItemData(hCurSel);
  GetDocument()->NewChild(this, pParent->CreateChild());   //this pointer so the doc 'knows' which view it came from (I assume you have multiple views on one doc)
}

or like this
{
  HTREEITEM hCurSel = GetTreeCtrl().GetCurSel();
  CItem* pItem = GetTreeCtrl().GetItemData(hCurSel);
switch(pItem->Type())
{
case _container:
  GetDocument()->NewChild(this, pItem->CreateChild());
  break;
case _item:
  GetDocument()->NewChild(this, pItem->GetParent()->CreateChild(pItem)); //Here the pItem gives the item before/after the one you are inserting to in the list
  break;
}
}

Avatar of mrwad99

ASKER

Hi Andy

Yes you are correct.  It is my fault that I did not fully understand what you were saying regarding this. I will probably end up giving you and Radu 500 points each for this due to the extensive help given.  

I will leave this question open for the minute until I get chance to implement it, but in the meantime I have another issue similar at Q_21388610.html that maybe you could help with.

Thanks.
I know it's not easy to understand when in words, unfortunately it is not something one can cobble a code example together in a few minutes.
Avatar of mrwad99

ASKER

Andy I have posted a question where you can get points for your help - http:Q_21394100.html  Please accept those; when you have I will close this question, accepting Radu's largest answer above.

I thank you both greatly for the help here, not only since I can move on with my project but also since I have learnt a valuable technique :)
Glad to have been able to help.

So, you got your notifications going then, and your views being updated?

And, do you now have a New Window command? All views always in sync? View update operations snappy?

It's good, isn't it?


MFC certainly is an old dog, but it's not as bad as some would have you believe :-)  Once you understand its Doc-View design and how to make use of the underlying framework.


Do you like the double dispatch pattern? It's quite powerful, really - performing type resolution without manual casts.
I think it’s worthwhile underlining one of its big advantages:
When you add a new command/notification, you get a compilation error if you do not provide support for it in the view/control class.

If we look back at CMyTreeView::OnUpdate, the method iterates over a collection of polymorphic pointers and invokes a virtual method on each one of them, which ends up delegating back to the CMyTreeView object – i.e. double dispatch.

Another possible solution would have been to dynamic_cast in OnUpdate:

void CMyTreeView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint)
{
     CUpdateHint* pUH = dynamic_cast<CUpdateHint*>(pHint);
     if (pUH != NULL)
     {
          const CUpdateHint::TreeCmdCollection_t& lpoCmds = pUH->GetTreeCommands();
          for (CUpdateHint::TreeCmdCollection_t::const_iterator itCmd=lpoCmds.begin(); itCmd!=lpoCmds.end(); ++itCmd)
          {
               CItemCreate* pCreate = dynamic_cast<CItemCreate*>(*itCmd);
               CItemDelete* pDelete = dynamic_cast<CItemDelete*>(*itCmd);
               if (pCreate != NULL)
               {
                    // process create
               }
               else if (pDelete != NULL)
               {
                    // process delete
               }
               else
               {
                    ASSERT(false);
                        // unsupported command
               }
          }
     }
     else
     {
          ASSERT(false);
     }
}

The problem with this approach is that if we add a new command and forget to implement support for it in the OnUpdate method, we won’t become aware of it unless we execute the debug version and test all possible commands. When we’re going to hit the new command, OnUpdate is going to assert and we’ll see we have to update OnUpdate (no pun intended). This is obviously not ideal, as it may slip through the cracks.


The other option, using the ApplyToView virtual method but implementing the update functionality in the command classes was already discussed in my previous post: it would require us to expose CMyTreeCtrl implementation details to the command classes. This is also not ideal for obvious reasons.


With the double dispatch approach, the command classes become very lean – they are just dumb containers. In fact, in our implementation we resorted to providing a macro to make it even easier for programmers to add command classes; something like

#define DECLARE_TREE_CMD                                                   \
public:                                                                                    \
      virtual void ApplyToView(CMyTreeCtrl* pCtrl) const    \
      {                                                                            \
            if (pCtrl != NULL)                                     \
                  pCtrl->ApplyCmd(this);            \
            else                                                       \
                  ASSERT(false);                      \
      }

Our macro contained a few other things in addition to the stock implementation of ApplyToView, but you get the idea.

With this macro in place, it becomes almost trivial to implement new command classes. There is no need to know anything about the views that get updated based on it:

class CItemMoved : public ITreeCommand
{
      DECLARE_TREE_CMD

      // command attributes
};

More importantly, if we forget to add the corresponding ApplyCmd in the tree control, we get a compilation error. Thus we find out about it immediately. This is very important when working in a team. It’s entirely possible for the command classes to be implemented by one programmer (e.g. in the core team) whereas the tree control may be another programmer’s responsibility (e.g. in the U/I team). The compilation error forces the U/I programmer to update his code immediately (as soon as the core team adds the new command).

 
Radu

Avatar of mrwad99

ASKER

Hi Radu

Yeah, I got the notifications going, with a new Window command, and both views are in sync.  It is good to watch them all update when I make changes to one :)

You are right, it certainly is very powerful.  And that macro definition, well, that is the icing on the cake really.  I would not have thought of that at all, let alone the advantages of it.

I thank you once again for all the help and the time you have invested in me with this question, I have learnt one heck of a lot.

:)