We currently have an application that allows users to create a Contract
. A contract can have 1 or more Project
. A project can have 0 or more sub-projects (which can have their own sub-projects, and so on) as well as 1 or more Line
. Lines can have any number of sub-lines (which can have their own sub-lines, and so on).
Currently, our design contains circular references, and I'd like to get away from that. Currently, it looks a bit like this:
public class Contract
{
public List<Project> Projects { get; set; }
}
public class Project
{
public Contract OwningContract { get; set; }
public Project ParentProject { get; set; }
public List<Project> SubProjects { get; set; }
public List<Line> Lines { get; set; }
}
public class Line
{
public Project OwningProject { get; set; }
public List ParentLine { get; set; }
public List<Line> SubLines { get; set; }
}
We're using the M-V-VM "pattern" and use these Models (and their associated view models) to populate a large "edit" screen where users can modify their contracts and the properties on all of the objects. Where things start to get confusing for me is when we add, for example, a Cost
property to the Line
. The issue is reflecting at the highest level (the contract) changes made to the lowest level.
Looking for some thoughts as to how to change this design to remove the circular references. One thought I had was that the contract would have a Dictionary<Guid, Project>
which would contain ALL projects (regardless of their level in hierarchy). The Project
would then have a Guid property called "Parent" which could be used to search the contract's dictionary for the parent object. THe same logic could be applied at the Line
level.
Thanks! Any help is appreciated.
1 Answer 1
Here are some thoughts:
- What you've described is your
Model
, not yourViewModel
. I've done MVVM on a tree structure before and it gets ugly fast. YourViewModel
needs to mimic this structure, but it should useObservableCollection
instead ofList
, and it can be more accepting of invalid data because the data's in transition while they're editing it. - Create code that will convert from your
Model
to yourViewModel
when you load the screen. That allows you to implement a Cancel button that just does nothing. - I used events to propagate changes at lower levels in the tree up to higher nodes, and I would recommend not doing that. It would be better if the child node just called a method on the parent node, like
PriceChanged(decimal oldPrice, decimal newPrice)
- Consider all the actions that have to propagate: new children, re-ordering children, deleting children.
- Decide up front if you want to implement Undo functionality because it's extremely hard to shoehorn it in afterwards. If you do want to have Undo, I suggest designing the tree such that every action has an easy and obvious reverse action, and then push that reverse action onto a
Stack<Action>
. You might want to pass a reference to thatStack
to every node in the tree so you don't have to pass the undo actions up the tree for storage. Remember that in making every action have a reverse action, you have to think aboutPropertyChanged
events too, so yourView
stays in sync. - You'll need code that validates the
ViewModel
before committing the changes to theModel
, and you'll need code that commits the changes. - Think of the whole task as a data structure problem. Pre-conditions, actions, post-conditions. If you haven't used test driven development before, I suggest this would be an excellent place to start using it.
-
Thanks for your response. I'll review your thoughts above. Do you have any recommendations for eliminating the circular references from the tree structure? That is, how can a child project know it's parent without directly having a reference to it's parent object?Thelonias– Thelonias2012年10月25日 19:29:45 +00:00Commented Oct 25, 2012 at 19:29
-
1I don't suggest eliminating them (it's a normal way to do a tree structure). However, the other way to do it is to store relationships as a separate set of entities. So a
Relationship
has aParent
property and aChild
property. Then the nodes of the tree don't know about each other. I don't think that's a great idea though, because it's that much harder for nodes to notify each other when things change.Scott Whitlock– Scott Whitlock2012年10月25日 19:35:54 +00:00Commented Oct 25, 2012 at 19:35 -
If I could, I'd give an extra +1 for not worrying about the circular references. They're not related to the challenges the question is addressing.user53019– user530192012年10月25日 21:05:51 +00:00Commented Oct 25, 2012 at 21:05
-
@GlenH7 The circular references do cause me some pain though because we're using the DataContractSerializer (saving to XML as well as sending to a WCF service). I know we can use an attribute to mark an object as containing a reference, but this gets ugly quickly working up the object hierarchy. I was just curious if there was a cleaner way of doing things. I suppose I don't really need a "Parent" property on the Model...I can handle communication back to the parent via events.Thelonias– Thelonias2012年10月26日 14:57:23 +00:00Commented Oct 26, 2012 at 14:57
-
@Ryan - please don't use events. You will eventually forget to unsubscribe one and then you just get weird stuff happening (consider the case when the user drags one node from one parent to another). If you don't want references, then use some kind of message bus (a common object that every node has a reference to where you can send a message like
PriceChanged
and all other nodes can use it as a trigger to recalculate prices, or do a top-down recalculation).Scott Whitlock– Scott Whitlock2012年10月26日 16:38:05 +00:00Commented Oct 26, 2012 at 16:38