The part persistency recipes are listed as follows:
OpenDoc provides a structured storage model which enables part editors to persistently store multiple representations of their content. To present this feature in a simple and easy to use manner to users, part editors must follow a few rules in general, and be more particular in their implementation of a handful of their methods. This document contains the list of part editor responsibilities, and sample code which would allow a part editor to fulfill these responsibilities.
The terms you need to be familiar with are:
Kind | Specific data format or type |
Category | Set of similar kinds. A kind can be in multiple categories. |
Preferred kind of a part | The preferred kind of a part designates the data format which the editor running the part must use to 'ternalize the part's content. The preferred kind is stored in kODPropPreferredKind property of the part's storage unit. If the kODPropPreferredKind property does not exist, then the editor can presume that the preferred kind of the part is the value type of the first value in the contents property (kODPropContents) of the part's storage unit. |
Registration | Registration of a part handler. See the "Dynamic Binding" recipes for more information. |
Standard kinds are those data formats which, either through an official decree or by some de facto means, have become widely used and accepted. Here are some examples:
Industry Standard Kinds | ASCII, TIFF, JPEG, etc. |
Supporting a standard kind has many advantages.
Supporting a standard kind means there is a greater chance that your editor can read parts created by other editors. This enables users to share documents created by your part editor.
To correctly support multiple kinds, there are methods your editor needs to ensure proper behavior.
This method is called when stationery is created. Your editor should make sure it does the following:
This method is called when a part is read back in from an existing document. This happens whenever a user opens a document or creates a new document. Your editor should make sure it does the following:
Note:
It is possible that your editor was bound to a part which previously had a different editor. In this case, the binding subsystem will automatically notify the user. In this case, if your editor does not support the preferred kind, then use the highest fidelity kind in the contents property which your editor does support as the de facto preferred kind. Do not update the preferred kind property until externalize or ChangeKind is called on your part.
This method may be called at any time. Depending on the Save model of the current document, and the idle-time optimizations which may or may not be present, your part may be told to externalize only when the user Saves a document, or as often as every minute. The point is, do not assume that when you are told to externalize, that it is for any one particular reason. As an optimization, your editor should probably keep an fDirty flag which is set whenever the user changes the part's content, and cleared whenever Externalization is completed. If your fDirty flag is clear then your externalize method should be a no op. Your editor should make sure it does the following:
This method is called when the user does a Save a copy as multiple formats, or when a data interchange utility goes through a document asking all the parts to externalize themselves in a set of kinds extracted from a set of standard interchange kinds. Your editor should make sure it does the following:
This method is called when the user changes the preferred kind of a part. This is usually done from the Part/Document Properties notebook, although do not assume it is the only user interface that can cause this method to be called. Your editor should make sure it does the following:
This method is similar to externalize. It is usually called in response to a data interchange action such as using the clipboard or drag and drop. See the "Data Interchange" recipes for precise detail about implementing the CloneInto method. For the purposes of the multiple kind support however, make sure your editor does the following:
Your part editor tells OpenDoc what kinds it supports in response to its clsGetODPartKinds metaclass method. This method should return a sequence of PartKindInfo structures that contain the appropriate information for each part kind.
Earlier we stated that the user should be the only thing which changes the preferred kind of a part. Here is how they do it. To change the preferred kind of an embedded part, the user does the following:
To change the preferred kind of the root part document, the user does the following:
In any discussion of preferred kinds, it makes sense to also describe preferred editors. Using the OpenDoc Preferences notebook the user can set a particular editor to be the preferred editor for a particular kind. Whenever the user encounters a part of that kind, whose previous editor is unavailable, the user set preferred editor for that kind is used to run that part. Similarly, the user can set a particular editor to be the preferred editor for a whole category, such as Other Plain Text. This means that whenever the user encounters a part whose kind is in the Plain Text category, whose previous editor is unavailable, and there is no preferred editor for that kind, the user set preferred editor for that category (Plain Text) is used.
Parts, like all other persistent objects have a storage unit in which they can store their content data and state persistently. However, parts are intimately involved in actions like binding, translation, data interchange (clipboard, drag and drop). As a result, parts need to satisfy many more requirements than the other persistent objects, and so they need to be more disciplined about how they use their storage units.
Values written out by parts must be in Little Endian format regardless of the endianess of the machine. OpenDoc provides a set of utility functions for writing standard type values to storage units. These functions will perform the data conversion to Little Endian format when necessary. The functions are:
Alternatively, if a part needs to write out values to the storage unit without using the functions listed above, OpenDoc provides a set of conversion macros. These macros have to be called BEFORE writing out the values to the storage unit. Following are the macros that have to be called:
ODSShort value = 12; ODSUForceFocus(ev, storageUnit, property, valueType); value = ConvertODSLongToStd(value); StorageUnitSetValue(storageUnit, ev, sizeof(ODSShort), &value); |
Similarly, values extracted from the storage unit have to be converted on a Big Endian machine before they can be used. Again, OpenDoc provides both utility functions and conversion macros for parts to use.
The utility functions read standard type values from storage units and will format the data in Big Endian format if necessary. They are:
If conversion macros are used, they should be called AFTER reading the values from the storage units. The list of conversion macros are:
Below is an example of how to use the conversion macro:
if (ODSUExistsThenFocus(ev, storageUnit, property, valueType); { ODSShort value; StorageUnitGetValue(storageUnit, ev, sizeof(ODSShort), &value); value = ConvertODSShortFromStd(value); } |
As a result of the above rules/requirements, parts store their contents as follows:
These values are ordered by fidelity, for example highest fidelity first, lowest fidelity last.
Into each of these values, the part streams out its contents using the appropriate data format.
One representation, one value.
No content should be stored outside of the contents property.
All other properties on a part's storage unit are for meta-information/annotations and should not contain content.
Example:
property OpenDoc:Property:Contents value OSA:Kind:TestDrag value OSA:Kind:StyledText value OSA:Kind:PlainText |
How do you tell the difference between annotations and content?
Answer:
There is often a misconception that there is a lot of overlap between annotations and content. In fact, it is difficult to come up with a precise definition which separates the two. However, more often than not, it is fairly easy to categorize any particular piece of information as annotation or content.
Here are some questions to ask yourself about a piece of info X which you are unsure of whether it is annotation or content.
If you answered yes to any of the above, then X is content.
But cannot we just distinguish meta-information from content by using a property naming convention, and store them both as sets of top level properties?
Answer:
No, storing content outside the contents property would violate requirements 3 and 4 above. Also, placing a more restrictive naming convention on property names would violate requirement 7 above.
But what if a part wants to store a particular representation using more than one property in a structured way?
Answer:
Any part may store a particular representation in a structured fashion by requesting additional storage units from the draft and tying them together with references.
Would not it be more efficient to keep the multiple properties in the part's storage unit instead of requesting another storage unit?
Answer:
It is hard to be more efficient when it does not even work! That approach would violate requirements 3 and 4. It is not a question of efficiency, it is a question of functionality.
Answer:
No, the part storage model is only as restrictive as you want it to be. If, like most applications, your part uses only a single stream to store its contents, then your storage model fits right into the part storage model, you simply use a single value to store your contents. However, if you are using resources or some other form of structured storage, then you can ask your storage unit's draft object for additional storage units, and then store references to these storage units inside your primary content value. See the persistent reference document for more information about using storage unit references. It is important to note that because you can always ask for more storage units and then save references to them in any value, you can construct any kind of arbitrary graph structure for your storage representation. This is much more powerful than current day files and streams. Each additional storage unit can have multiple properties (you may think of these as multiple forks), and each property can have multiple values (again, different values within the same property should only be used for multiple representations of the same data).
Also, note that:
This document contains some sample code showing how a part editor externalizes and internalizes itself.
The sample code shown here is for a very simple part editor. Its assumptions may not be valid for a part editor with more functionality:
The part is responsible for caching the draft permissions in InitPartFromStorage. The sample code does this by assigning a field:
fPermissions = storageUnit->GetDraft(ev)->GetPermissions(ev); |
In addition to a storage unit, InitPart and InitPartFromStorage methods on ODPart also takes an extra parameter, partWrapper. The partWrapper parameter represents a delegator object which OpenDoc uses to insulate the rest of OpenDoc from having to have a direct pointer to your ODPart object. This feature helps implement features such as being able to change the editor of part without closing and reopening the document. Plus, it makes an excellent bottleneck for platform developers. For the most part, all the part wrapper does is delegate methods of the part function to your ODPart object.
The part editor must save this parameter in the part's internal field; you will see this in the sample code below. Whenever an OpenDoc function requires the part editor to pass in an ODPart* representing the part, instead of passing in somSelf, the part editor must pass in partWrapper stored in the part's internal field. An example is when registering the part for idle time. A part editor should never pass somSelf in as a parameter to an OpenDoc function call.
// Includes the appropriate .XH files from OpenDoc #include <StorageU.xh> #include <StdProps.xh> #include <POUtils.h> ... // Define the part kind. // This should probably be in your part's ....def.H file which should // be #included in your .r file for your nmap resources #define kMyPartKind "Sample:Kind:CMyKind" ... void MyPartInitPart(MyPart* somSelf, Environment* ev, ODStorageUnit* storageUnit, ODPart* partWrapper) { ... // Call your parent's init routine parent_InitPart(somSelf, ev, storageUnit, partWrapper); // Set your dirty flag fDirty = kODTrue; // Save off the part-wrapper object fSelf = partWrapper; } void MyPartInitPartFromStorage(MyPart* somSelf, Environment* ev, ODStorageUnit* storageUnit, ODPartWrapper* partWrapper) { // Initialize the parent parent_InitPartFromStorage(somSelf, ev, storageUnit, partWrapper); // Focus to the value where the part Data is stored storageUnit->Focus(ev, kODPropContents, kODPosUndefined, kMyPartKind, 0, kODPosUndefined); // Get the size of the string fPartData[0] = storageUnit->GetSize(ev); // Get the string itself storageUnit->GetValue(ev, fPartData[0],&(fPartData[1])); // Save off the part-wrapper object fSelf = partWrapper; // Note the draft permissions, in order to avoid // writing to a read-only draft fPermissions = storageUnit->GetDraft(ev)->GetPermissions(ev); // Set the dirty flag to false fDirty = kODFalse; } |
void MyPartExternalize(MyPart* somSelf, Environment* ev) { // Only externalize when the part has been updated. // Externalize the parent. parent_Externalize(somSelf, ev); if (fDirty) { // Get the part's storage unit ODStorageUnit* storageUnit = somSelf->GetStorageUnit(ev); if (storageUnit->Exists(ev, kODPropContents, kMyPartKind, 0)) { // Focus to the desired value storageUnit->Focus(ev, kODPropContents, kODPosUndefined, kMyPartKind, 0, kODPosUndefined); } else { storageUnit->AddProperty(ev, kODPropContents); storageUnit->AddValue(ev, kMyPartKind); // Adding a property and value has the side effect of focusing // the storage unit to that value. } // Write out the part data storageUnit->SetValue(ev, fPartData[0],&(fPartData[1])); // Set the dirty flag to kODFalse to signify that the part is clean fDirty = kODFalse; } } |
Parts can attach part info data to their display frames. This data can be used to hold any information the part wants to associate with the display frame, and is stored persistently in the frame's storage unit. While the data is stored with the frame, the part editor is responsible for its 'ternalization, as the editor is the only entity that understands the structure and format of the part info.
A frame's part info is stored in the frame's storage unit in the part info property. The standard name for this property is found in the constant kODPropPartInfo. Just as a part editor stores multiple representations of a part's contents in multiple values in the contents property, the editor should also store multiple representations of the part info which correspond to the representations of the contents. Some content formats may not have corresponding part info, especially interchange formats like JPEG or RTF. However, many editors will define formats with corresponding part info, and if your editor supports another editor's native format directly, it should also support the part info for that format.
The names of part info formats which are linked to a content format should be the same as the name of the content format, with the segment PartInfo appended to the end of the ISO name. For example, if your part content format was SurfSoft:SurfText 1.0 the part info should be SurfSoft:SurfText1.0:PartInfo.
For a part which externalized representations for the formats SurfText 1.0, RTF, ClarisWorks 3.0, and ASCII, its display frames should have part info with formats SurfSoft:SurfText1.0:PartInfo and Claris:ClarisWorks 3.0:PartInfo (assuming the interchange formats have no associated part info).
There are three calls in the part function that deal with 'ternalization of part info:
In all of these calls, the frame passes a StorageUnitView which is focused to the frame's part info property, but not to any value within that property. Your part editor must get the view's storage unit, and focus it to value(s) in that property as necessary to read or write the formats it needs to.
When externalizing a property of a persistent object, it is only necessary to write the data if it is the first time writing it, or if it has changed from the last time the object was externalized. As an optimization, you can keep an isDirty flag in a frame's part info, indicating whether it has changed from when it was internalized or last externalized. In WritePartInfo, if the flag is false, skip externalizing; if the flag is true, externalize the data and set the flag to false. Note that you must always externalize part info when cloning (in ClonePartInfo), as you are making a new copy.
Just as a part must mark the draft as dirty when its content needs to be externalized, it must also mark the draft dirty when part info needs to be externalized. The part should call draft->SetChangedFromPrev() when part info has changed and needs to be externalized. Note that changes to part info in a non-persistent frame should not mark the draft dirty.
This code 'ternalizes a simple part info structure. The only value in it is an RGB color describing the background color of the frame. The forDebuggingRGBColor intermediate value makes debugging easier.
#include <ISOStr.h> #include <StorageU.xh> #include <SUView.xh> #include <StorUtil.h> ... ODInfoType CMyPart::ReadPartInfo(Environment* ev, ODFrame* frame, ODStorageUnitView* storageUnitView) { ODStorageUnit* su = storageUnitView->GetStorageUnit(ev); ODPropertyName propName = storageUnitView->GetProperty(ev); PartInfoRec* partInfo = kODNULL; if (ODSUExistsThenFocus(ev, su, propName, kKindSamplePartInfo)) { partInfo = new PartInfoRec; RGBColor forDebuggingRGBColor ; StorageUnitGetValue(su, ev, sizeof(RGBColor), (ODValue)&forDebuggingRGBColor); partInfo->bgColor = forDebuggingRGBColor; partInfo->isDirty = kODFalse; } SOMFree(propName); return (ODInfoType)partInfo; } void CMyPart::WritePartInfo(Environment *ev, ODInfoType partInfo, ODStorageUnitView* storageUnitView) { if (partInfo && partInfo->isDirty) { ODStorageUnit* su = storageUnitView->GetStorageUnit(ev); ODPropertyName propName = storageUnitView->GetProperty(ev); ODSUForceFocus(ev, su, propName, kKindTestDraw); RGBColor forDebuggingRGBColor ; forDebuggingRGBColor = ((PartInfoRec*)partInfo)->bgColor; StorageUnitSetValue(su, ev, sizeof(RGBColor), (ODValue)&forDebuggingRGBColor); partInfo->isDirty = kODFalse; SOMFree(propName); } } void CMyPart::ClonePartInfo(Environment *ev, ODInfoType partInfo, ODStorageUnitView* storageUnitView) { if (partInfo) { ODStorageUnit* su = storageUnitView->GetStorageUnit(ev); ODPropertyName propName = storageUnitView->GetProperty(ev); ODSUForceFocus(ev, su, propName, kKindTestDraw); RGBColor forDebuggingRGBColor ; forDebuggingRGBColor = ((PartInfoRec*)partInfo)->bgColor; StorageUnitSetValue(su, ev, sizeof(RGBColor), (ODValue)&forDebuggingRGBColor); SOMFree(propName); } } |
A well behaved part editor needs to keep track of its display frames, and store and retrieve them from an annotation property on its storage unit. This document contains the list of methods affected, and a brief description of their responsibilities. The sample code documents step by step how to implement this functionality in those methods.
The following sample code:
Constructor | Clear fDisplayFrames. |
Destructor | Delete fDisplayFrames if it is not null, and set it to null. |
InitPart | Create a new, empty collection (fDisplayFrames) in which to store a list of your display frames. |
DisplayFrameAdded | Add the frame passed in to fDisplayFrames. |
DisplayFrameConnected | Add the frame passed in to fDisplayFrames if it is not already in the list. |
DisplayFrameRemoved | Remove the frame passed in from fDisplayFrames. |
DisplayFrameClosed | Remove the frame passed in from fDisplayFrames. |
Externalize | If necessary create a DisplayFrames annotation property and StorageUnitRefs value on your storage unit. Otherwise just focus to your DisplayFrames annotation property and its StorageUnitRefs value. Write out a list of references to your display frames from fDisplayFrames. |
InitPartFromStorage | Focus to your DisplayFrames annotation property and its StorageUnitRefs value. Read in the list of your display frames into fDisplayFrames. See the "Lazy Frame Internalization" recipe for detail. |
ReleaseAll | Assert that you fDisplayFrames collection is empty. |
void CMyPart::CMyPart() { ... fDisplayFrames = kODNULL; ... } void CMyPart::~CMyPart() { ... if (fDisplayFrames != kODNULL) { delete fDisplayFrames; fDisplayFrames = kODNULL; } ... } void CMyPart::InitPart(Environment* ev, ODStorageUnit* storageUnit, ODPart* partWrapper) { ... // Create a list to keep track of the frames we are being // displayed in fDisplayFrames = new CLinkedList; ... } void CMyPart::InitPartFromStorage(Environment* ev, ODStorageUnit* storageUnit, ODPart* partWrapper) { ... // Create a list to keep track of the frames we are being // displayed in fDisplayFrames = new CLinkedList; // Read in our list of display frames storageUnit->Focus(ev, kODPropDisplayFrames, kODPosUndefined, kODWeakStorageUnitRefs, 0, kODPosUndefined); ODULong size = storageUnit->GetSize(ev); storageUnit->SetOffset(ev, 0); for (ODULong offset = 0; offset < size; offset += sizeof(ODStorageUnitRef)) { StorageUnitGetValue(storageUnit, ev, sizeof(ODStorageUnitRef), weakRef); if (storageUnit->IsValidStorageUnitRef(ev, weakRef)) { ODFrame *frame = storageUnit-> GetDraft(ev)-> AcquireFrame(ev, // Calls my DisplayFrameConnected storageUnit-> GetIDFromStorageUnitRef(ev, weakRef)); frame->Release(ev); } } ... } void CMyPart::DisplayFrameAdded(Environment* ev, ODFrame* frame) { ... frame->IncrementRefCount(ev); fDisplayFrames->Add((ODPtr)frame); ... } void CMyPart::DisplayFrameConnected(Environment* ev, ODFrame* frame) { ... if (!fDisplayFrames->Contains((ODPtr)frame)) { frame->IncrementRefCount(ev); fDisplayFrames->Add((ODPtr)frame); } ... } void CMyPart::DisplayFrameRemoved(Environment* ev, ODFrame* frame) { ... fDisplayFrames->Remove((ODPtr)frame); frame->Release(ev); ... } void CMyPart::DisplayFrameClosed(Environment* ev, ODFrame* frame) { ... fDisplayFrames->Remove((ODPtr)frame); frame->Release(ev); ... } void CMyPart::Externalize(Environment* ev) { ... ODStorageUnitRef weakRef; ODStorageUnit* su = this->GetStorageUnit(ev); ODSUForceFocus(su, kODPropDisplayFrames, kODWeakStorageUnitRefs); // Persistent object references are stored in a side table, rather than // in the property/value stream. // Thus, deleting the contents of a value will not delete the references // previously written to that value. // To completely delete all references written to the value, we must // remove the value and add it back. su->Remove(ev); su->AddValue(ev, kODWeakStorageUnitRefs); for (CLink* link = fDisplayFrames->First(); link->Content(); link = link->Next()) { frame = (ODFrame*) link->Content(); frameID = frame->GetID(ev); // Write out weak references to each of the part's display frames su->GetWeakStorageUnitRef(ev, frameID, weakRef); StorageUnitSetValue(su, ev, sizeof(ODStorageUnitRef), weakRef); } ... } void CMyPart::ReleaseAll(Environment* ev) { ... ASSERT(fDisplayFrames->First() == kODNULL); ... } |
Winning through laziness:
There are situations in OpenDoc where eagerly doing work as soon as possible is not the best thing to do, and the work should be deferred until it really needs to be done. Internalization is usually a good candidate for being lazy. In general, do not internalize data until it is needed, or you can predict it will be needed soon (for example you might pre-fetch the next page so scrolling will be fast). A particular case of lazy internalization involves your part's frames, both display frames and embedded frames.
Since OpenDoc places very few requirements on a part's internal representation, your part is free to organize its data however, it pleases. The only requirements relevant to frames are that your part must be able to supply an EmbeddedFramesIterator for its embedded frames, and it should externalize a list of its display frames in a standard property of its storage unit.
For very simple containing parts, there will not be much advantage in lazily internalizing embedded frames. But for large containing parts, the advantages can be quite significant. For example, a containing part which embeds five thousand frames (think CD-ROM here) would not want to internalize all of those frames at the same time. There would be no way to display all the frames simultaneously, and a large amount of memory would effectively be wasted on keeping those persistent objects resident. All parts should consider lazy internalization of display frames, since your part has no control over the number of display frames that may be added to it.
The main trick to lazy frame internalization is stopping before you go too far. In InitPartFromStorage, you should stop short of calling draft->GetFrame(). Instead, just hang on to the frame ID that you got from the storageUnitRef. (Remember that display frame references should be weak, so they may not actually have a frame or ID at the other end). The minimal way to deal with this ID is to create a structure that can hold a frame pointer and a frame ID. Anywhere in your code you would have stored a frame reference, store one of these structures instead. If the structure does not have a frame yet, use the ID to get it, then store it in the structure for next time (Remember to increment the frame's reference count.)
For slightly more effort up-front, you can have a much easier time of it later. You can create a class that stores a frame reference and a frame ID, and does the GetFrame for you when necessary. The class Futon below is an example of such. (Do not try to read any significance into the name. It is just a random word that was easy to type).
Now that your part is more relaxed about internalizing its frames, here is how it can take advantage of being lazy.
#define INCL_ODAPI #define INCL_ODFRAME #define INCL_ODDRAFT #include <ODos2.h> #include <ODUtils.h> class Futon { public: Futon(ODDraft* draft, ODID frameID); ~Futon(); ODFrame* GetFrame(Environment* ev); void Purge(Environment* ev); ODDraft* fDraft; ODID fFrameID; ODFrame* fFrame; }; Futon::Futon(ODDraft* draft, ODID frameID) { // Note: // I am not reference counting the draft, because the draft is // referenced from the part's storage unit, so it is // not going away while the Futon (and thus the part) // is still around. fDraft = draft; fFrameID = frameID; fFrame = kODNULL; } Futon::~Futon() { ODSafeReleaseObject(fFrame); } ODFrame* Futon::GetFrame(Environment* ev) { if (fFrame == kODNULL) fFrame = fDraft->GetFrame(ev, fFrameID); return fFrame; } void Futon::Purge(Environment* ev) { ODReleaseObject(ev, fFrame); } |