<> <> <> <> <> <<(Corrected destruction of viewer properties when playing log; simplified CarefullyApply)>> <<(Changed reset to close transaction before doing redisplay so that cached pages aren't used -- this gets the proper stuff redisplayed!)>> <<(Added WBError and changed error handling behavior in the code -- now most of it is in CarefullyApply, where it should have been all along)>> <> DIRECTORY Atom, BasicTime, Booting, DB, DBDefs USING[Entity], DBIcons, Nut, Process, Rope, ViewerClasses USING [Viewer, ViewerRec], ViewerLocks USING [CallUnderWriteLock], ViewerOps, ViewerTools, UserCredentials, WhiteboardDB, WhiteboardDBPrivate, WhiteboardDump, WhiteboardLoad, WhiteboardViewers; WhiteboardDBImpl: CEDAR MONITOR IMPORTS BasicTime, Booting, DB, DBIcons, Nut, Process, Rope, ViewerLocks, ViewerOps, ViewerTools, WhiteboardDBPrivate, WhiteboardViewers, WhiteboardDump, WhiteboardLoad EXPORTS WhiteboardDB = BEGIN OPEN DB, WhiteboardDBPrivate; Viewer: TYPE = ViewerClasses.Viewer; ROPE: TYPE = Rope.ROPE; WhiteboardLog: TYPE = LIST OF WhiteboardLogEntry; WhiteboardLogEntry: TYPE = RECORD[action: ATOM, to: Viewer]; <> <<$NewIcon>> <<$NewBox>> <<$Delete>> <<$EditText>> <<$Grow>> <<$Move>> <<$Erase>> whiteboard: PUBLIC ATOM _ WhiteboardDBPrivate.wbType; destroyedList: LIST OF Viewer; <> ticksToWait: Process.Ticks _ Process.SecondsToTicks[5*60]; stopped: PUBLIC BOOL _ FALSE; -- true if the Rollback proc has been executed and the transaction is to remain closed doingRestart: BOOL _ FALSE; -- true during the restart process, to keep iconic viewers from being opened activity: BOOLEAN _ TRUE; -- true if a database operation has been performed recently WBError: PUBLIC ERROR[reason: ATOM] = CODE; -- the error that may be raised from this interface WatchDBActivity: PROC[] = { <> WHILE TRUE DO Process.Pause[ticksToWait]; CheckConnection[] ENDLOOP }; CheckConnection: ENTRY PROC[] = { ENABLE UNWIND => NULL; aborted: BOOLEAN _ FALSE; IF stopped THEN RETURN; IF NOT activity THEN WhiteboardDBPrivate.CloseTransaction[ ]; activity _ FALSE }; Close: PUBLIC ENTRY PROC[] = { ENABLE UNWIND => NULL; WhiteboardDBPrivate.CloseTransaction[ ]; stopped _ TRUE }; <> Display: PUBLIC ENTRY PROC[eName: ROPE, v: Viewer _ NIL] RETURNS [viewer: Viewer] = BEGIN ENABLE UNWIND => NULL; DoDisplay: PROC[] = { viewer _ InternalDisplay[eName, v] }; CarefullyApply[DoDisplay] END; InternalDisplay: PROC[eName: ROPE, v: Viewer _ NIL] RETURNS [Viewer] = BEGIN locked: BOOLEAN _ FALSE; wb: Whiteboard = DeclareWhiteboard[eName]; children: ChildSet = Children[wb]; newViewer: BOOL _ FALSE; nextChild: WBItem; child: Viewer; x, y: INT; DisplayNoteEntity: PROC[] = BEGIN text: ViewerTools.TiogaContents = GetContents[nextChild]; w, h: INT; [w, h] _ Size[nextChild]; child _ WhiteboardViewers.AddTextBox[v, x, y, w, h, text]; ViewerOps.PaintViewer[child, all] END; DisplayIconEntity: PROC[] = BEGIN label, icon: ROPE; [label, icon] _ GetDisplayProps[nextChild]; child _ WhiteboardViewers.AddIcon[v, label, DBIcons.GetIcon[icon, tool], x, y]; END; DoDisplay: PROC = { versionNumber: INTEGER; <> IF NOT v.newVersion THEN { v.newVersion _ TRUE; ViewerOps.PaintViewer[v, caption]; WhiteboardViewers.SetMenu[v]; WhiteboardViewers.SetWBName[v, eName]; }; <> BEGIN gridSize: INT = GetGridSize[wb]; WhiteboardViewers.ResetGrid[v, gridSize] END; <> versionNumber _ GetVersion[wb]; ViewerOps.AddProp[v, $Version, NEW[INTEGER _ versionNumber]]; WHILE (nextChild _ NextChild[children]) # NIL DO [x, y] _ Position[nextChild]; IF TypeOf[nextChild] = note THEN DisplayNoteEntity[] ELSE DisplayIconEntity[]; StoreEntity[child, DB.EntityInfo[nextChild].name]; ENDLOOP; v.newVersion _ FALSE; ViewerOps.PaintViewer[v, caption] }; BEGIN ENABLE UNWIND => { IF newViewer THEN ViewerOps.DestroyViewer[v] }; IF v = NIL OR v.class.flavor # WhiteboardViewers.wb THEN { v _ ViewerOps.CreateViewer[flavor: WhiteboardViewers.wb, info: [name: eName, iconic: TRUE]]; newViewer _ TRUE } ELSE { v.child _ NIL; ViewerOps.PaintViewer[v, client] }; ViewerOps.AddProp[v, $Log, NIL]; -- establish log [] _ Nut.SetFrozenProperty[v, FALSE]; Nut.SetNutInfo[v, $Whiteboard, "Whiteboard", eName]; ViewerOps.AddProp[v, $Lines, NIL]; ViewerLocks.CallUnderWriteLock[DoDisplay, v]; IF NOT doingRestart AND v.iconic AND NOT v.destroyed THEN ViewerOps.OpenIcon[v]; RETURN[v] END END; Destroy: PUBLIC ENTRY PROC[eName: ROPE] = BEGIN ENABLE UNWIND => NULL; DoDestroy: PROC[] = { wb: Whiteboard = IF Exists[eName] THEN DeclareWhiteboard[eName] ELSE NIL; IF WhiteboardDBPrivate.readOnly THEN WBError[$ReadOnly]; IF wb = NIL THEN RETURN ELSE DestroyWhiteboard[wb]; WhiteboardDBPrivate.MarkTransaction[] }; v: Viewer; CarefullyApply[DoDestroy]; <> v _ ViewerOps.FindViewer[Rope.Cat["Whiteboard: ", eName]]; IF v # NIL THEN ViewerOps.DestroyViewer[v]; END; New: PUBLIC ENTRY PROC[eName: ROPE] RETURNS[success: BOOL] = BEGIN ENABLE UNWIND => NULL; wbEntity: DBDefs.Entity; DoNew: PROC[] = { IF WhiteboardDBPrivate.readOnly THEN WBError[$ReadOnly]; wbEntity _ DeclareWhiteboard[eName]; WhiteboardDBPrivate.MarkTransaction[] }; CarefullyApply[DoNew]; success _ wbEntity # NIL END; Reset: PUBLIC ENTRY PROC[wb: Viewer] = BEGIN ENABLE UNWIND => NULL; wb.newVersion _ TRUE; ViewerOps.PaintViewer[wb, caption]; WhiteboardDBPrivate.CloseTransaction[]; <> NARROW[ViewerOps.FetchProp[wb, $Version], REF INTEGER]^ _ -1; InternalReset[wb]; END; InternalReset: INTERNAL PROC[wb: Viewer] = BEGIN DoRedisplay: PROC[] = { name: ROPE = GetWBName[wb]; IF WhiteboardDBPrivate.Exists[name] THEN { wbEntity: Whiteboard = DeclareWhiteboard[name]; version: INTEGER = GetVersion[wbEntity]; IF version # NARROW[ViewerOps.FetchProp[wb, $Version], REF INTEGER]^ THEN [] _ InternalDisplay[name, wb] } ELSE ViewerOps.DestroyViewer[wb] }; CarefullyApply[DoRedisplay]; END; Save: PUBLIC ENTRY PROC[wb: Viewer] = BEGIN ENABLE UNWIND => NULL; eventLog: WhiteboardLog = NARROW[ViewerOps.FetchProp[wb, $Log], WhiteboardLog]; versionNumber: INTEGER = NARROW[ViewerOps.FetchProp[wb, $Version], REF INTEGER]^; name: Rope.ROPE = GetWBName[wb]; DoSave: PROC[] = { IF WhiteboardDBPrivate.readOnly THEN WBError[$ReadOnly]; BEGIN wbEntity: Whiteboard _ DeclareWhiteboard[name]; IF GetVersion[wbEntity] # versionNumber THEN WBError[$WrongVersion]; FOR log: WhiteboardLog _ eventLog, log.rest UNTIL log = NIL DO SELECT log.first.action FROM $NewIcon => DoNewIcon[wbEntity, log.first.to]; $NewBox => DoNewBox[wbEntity, log.first.to]; $Delete => DoDelete[log.first.to]; $EditText => DoEditText[log.first.to]; $Grow => DoGrow[log.first.to]; $Move => DoMove[log.first.to]; $Erase => wbEntity _ DoErase[wbEntity]; <> ENDCASE; ENDLOOP; SetGridSize[wbEntity, WhiteboardViewers.GetGrid[wb]]; SetVersion[wbEntity, versionNumber + 1]; SetCreateDate[wbEntity, BasicTime.Now[]]; WhiteboardDBPrivate.MarkTransaction[] END }; [] _ wb.class.scroll[wb, thumb, 0]; CarefullyApply[DoSave]; <> DestroyViewerList[]; [] _ InternalDisplay[name, wb] END; DestroyViewerList: PROC[] = { FOR l: LIST OF Viewer _ destroyedList, l.rest UNTIL l = NIL DO l.first.props _ NIL; l.first.data _ NIL ENDLOOP; destroyedList _ NIL }; GetWBName: PUBLIC PROC[wb: Viewer] RETURNS [name: ROPE] = BEGIN domain: ROPE; [domain ~ domain, entity ~ name] _ Nut.GetNutInfo[wb]; IF Rope.Equal[domain, "Whiteboard"] THEN RETURN; RETURN[NIL] END; WBExists: PUBLIC ENTRY PROC[eName: ROPE] RETURNS [alreadyExists: BOOL] = BEGIN ENABLE UNWIND => NULL; DoCheck: PROC[] = {alreadyExists _ Exists[eName]}; CarefullyApply[DoCheck]; END; GetCreateDate: PUBLIC ENTRY PROC[eName: ROPE] RETURNS [date: BasicTime.GMT] = BEGIN ENABLE UNWIND => NULL; DoGet: PROC[] = { wb: Whiteboard = DeclareWhiteboard[eName]; date _ WhiteboardDBPrivate.GetCreateDate[wb]}; CarefullyApply[DoGet]; END; CopyWB: PUBLIC ENTRY PROC[from, to: ROPE] = BEGIN ENABLE UNWIND => NULL; DoCopy: INTERNAL PROC[] = { IF WhiteboardDBPrivate.readOnly THEN WBError[$ReadOnly]; BEGIN fromWB: Whiteboard = DeclareWhiteboard[from]; toWB: Whiteboard = DeclareWhiteboard[to]; children: ChildSet = Children[fromWB]; FOR child: WBItem _ NextChild[children], NextChild[children] UNTIL child = NIL DO IF TypeOf[child] = note THEN CopyNote[toWB, child] ELSE CopyIcon[toWB, child] ENDLOOP; <> WhiteboardDBPrivate.MarkTransaction[] END }; CarefullyApply[DoCopy] END; CopyNote: INTERNAL PROC[to: Whiteboard, note: WBNote] = BEGIN text: ViewerTools.TiogaContents = GetContents[note]; x, y: INT; w, h: INT; [x, y] _ Position[note]; [w, h] _ Size[note]; [] _ NewNote[to, x, y, w, h, text] END; CopyIcon: INTERNAL PROC[to: Whiteboard, icon: WBIcon] = BEGIN x, y: INT; name, label, iconName, argument: ROPE; type: ATOM; [x, y] _ Position[icon]; [label, iconName] _ GetDisplayProps[icon]; [name, type] _ WhiteboardDBPrivate.GetIconProps[icon]; argument _ WhiteboardDBPrivate.GetToolArgument[icon]; [] _ WhiteboardDBPrivate.NewIcon[to, x, y, name, label, iconName, argument, type] END; GetChildren: PUBLIC ENTRY PROC[eName: ROPE _ NIL] RETURNS [wbList: LIST OF ROPE] = BEGIN ENABLE UNWIND => NULL; EnumWBs: PROC[] = { entityList: LIST OF WBItem; children: ChildSet = Children[DeclareWhiteboard[eName], TRUE]; FOR child: WBItem _ NextChild[children], NextChild[children] UNTIL child = NIL DO entityList _ CONS[child, entityList]; ENDLOOP; FOR eL: LIST OF Whiteboard _ entityList, eL.rest UNTIL eL = NIL DO wbList _ CONS[WhiteboardDBPrivate.GetIconProps[eL.first].name, wbList]; ENDLOOP }; CarefullyApply[EnumWBs] END; Enumerate: PUBLIC ENTRY PROC[pattern: ROPE _ NIL] RETURNS [wbList: LIST OF ROPE] = BEGIN ENABLE UNWIND => NULL; EnumWBs: PROC[] = { entityList: LIST OF Whiteboard = WhiteboardDBPrivate.Enumerate[pattern]; FOR eL: LIST OF Whiteboard _ entityList, eL.rest UNTIL eL = NIL DO wbList _ CONS[DB.EntityInfo[eL.first].name, wbList]; ENDLOOP }; CarefullyApply[EnumWBs] END; Dump: PUBLIC ENTRY PROC[to: ROPE] = { ENABLE UNWIND => NULL; DoDump: INTERNAL PROC[] = {WhiteboardDump.DumpToFile[to] }; CarefullyApply[DoDump] }; Load: PUBLIC ENTRY PROC[from: ROPE] = { ENABLE UNWIND => NULL; DoLoad: INTERNAL PROC[] = { IF WhiteboardDBPrivate.readOnly THEN WBError[$ReadOnly]; WhiteboardLoad.LoadFromFile[from] }; CarefullyApply[DoLoad] }; <> NewIcon: PUBLIC ENTRY PROC[wb: Viewer, x, y: INT, name: ROPE, type: ATOM, icon: ROPE, label: ROPE _ NIL, argument: ROPE _ NIL] RETURNS [new: Viewer] = BEGIN ENABLE UNWIND => NULL; IF Rope.Equal[label, ""] THEN label _ argument; new _ WhiteboardViewers.AddIcon[wb, IF Rope.Equal[label, ""] THEN name ELSE label, DBIcons.GetIcon[icon, tool], x, y]; ViewerOps.AddProp[new, $EntityName, name]; ViewerOps.AddProp[new, $IconLabel, label]; ViewerOps.AddProp[new, $IconType, type]; ViewerOps.AddProp[new, $IconIcon, icon]; ViewerOps.AddProp[new, $IconArgument, argument]; LogAction[wb, $NewIcon, new]; ViewerOps.PaintViewer[wb, caption]; END; NewBox: PUBLIC ENTRY PROC[wb: Viewer, x, y, w, h: INT, contents: ViewerTools.TiogaContents _ NIL] RETURNS [new: Viewer] = BEGIN ENABLE UNWIND => NULL; new _ WhiteboardViewers.AddTextBox[wb, x, y, w, h, contents]; LogAction[wb, $NewBox, new]; ViewerOps.PaintViewer[new, all]; ViewerOps.PaintViewer[wb, caption]; END; Delete: PUBLIC ENTRY PROC[child: Viewer] = BEGIN ENABLE UNWIND => NULL; logIt: BOOL = NOT WhiteboardViewers.DontLog[child]; props: Atom.PropList = child.props; data: REF ANY = child.data; <> IF logIt THEN LogAction[child.parent, $Delete, child]; ViewerOps.DestroyViewer[viewer: child, paint: FALSE]; IF logIt THEN {child.props _ props; child.data _ data} END; EditText: PUBLIC ENTRY PROC[box: Viewer] = BEGIN ENABLE UNWIND => NULL; LogAction[box.parent, $EditText, box]; END; Grow: PUBLIC ENTRY PROC[box: Viewer] = BEGIN ENABLE UNWIND => NULL; LogAction[box.parent, $Grow, box]; END; Move: PUBLIC ENTRY PROC[child: Viewer] = BEGIN ENABLE UNWIND => NULL; IF NOT WhiteboardViewers.DontLog[child] THEN LogAction[child.parent, $Move, child]; END; Erase: PUBLIC ENTRY PROC[wb: Viewer] = BEGIN ENABLE UNWIND => NULL; LogAction[wb, $Erase, wb]; wb.child _ NIL; -- flushes viewer ViewerOps.PaintViewer[wb, client]; END; LogAction: PROC[wb: Viewer, action: ATOM, v: Viewer] = BEGIN log: WhiteboardLog _ NARROW[ViewerOps.FetchProp[wb, $Log], WhiteboardLog]; AddToEnd: PROC[log: WhiteboardLog, entry: WhiteboardLogEntry] RETURNS[WhiteboardLog] = { IF log = NIL THEN RETURN[LIST[entry]] ELSE RETURN[CONS[log.first, AddToEnd[log.rest, entry]]] }; log _ AddToEnd[log, [action, v]]; ViewerOps.AddProp[wb, $Log, log]; END; <> DoNewIcon: PROC[wb: Whiteboard, child: Viewer] = <> BEGIN name: ROPE = NARROW[ViewerOps.FetchProp[child, $EntityName], ROPE]; label: ROPE = NARROW[ViewerOps.FetchProp[child, $IconLabel], ROPE]; type: ATOM = NARROW[ViewerOps.FetchProp[child, $IconType], ATOM]; icon: ROPE = NARROW[ViewerOps.FetchProp[child, $IconIcon], ROPE]; argument: ROPE = NARROW[ViewerOps.FetchProp[child, $IconArgument], ROPE]; newIcon: WBItem = WhiteboardDBPrivate.NewIcon[wb, child.wx, child.wy, name, label, icon, argument, type]; StoreEntity[child, DB.EntityInfo[newIcon].name] -- in case subsequent operations in the log reference this viewer (eg., to move it) END; DoNewBox: PROC[wb: Whiteboard, box: Viewer] = <> BEGIN text: ViewerTools.TiogaContents = ViewerTools.GetTiogaContents[box]; newNote: WBItem = WhiteboardDBPrivate.NewNote[wb, box.wx, box.wy, box.ww, box.wh, text]; StoreEntity[box, DB.EntityInfo[newNote].name] -- in case subsequent operations in the log reference this viewer (eg., to move it) END; DoDelete: PROC[child: Viewer] = <> BEGIN entity: WBItem = FetchEntity[child]; WhiteboardDBPrivate.Destroy[entity]; <> destroyedList _ CONS[child, destroyedList]; END; DoEditText: PROC[box: Viewer] = <> BEGIN note: WBNote = FetchEntity[box]; text: ViewerTools.TiogaContents = ViewerTools.GetTiogaContents[box]; SetContents[note, text] END; DoGrow: PROC[box: Viewer] = <> BEGIN note: WBNote = FetchEntity[box]; WhiteboardDBPrivate.Grow[note, box.ww, box.wh] END; DoMove: PROC[child: Viewer] = <> BEGIN entity: WBItem = FetchEntity[child]; WhiteboardDBPrivate.Move[entity, child.wx, child.wy] END; DoErase: PROC[wb: Whiteboard] RETURNS[newWB: Whiteboard] = <> BEGIN name: ROPE = NameOf[wb]; WhiteboardDBPrivate.DestroyWhiteboard[wb]; newWB _ DeclareWhiteboard[name] END; <> FetchEntity: PUBLIC PROC[v: Viewer] RETURNS[e: WBItem] = { name: ROPE = NARROW[ViewerOps.FetchProp[v, $Entity], ROPE]; IF name = NIL THEN RETURN[NIL]; SELECT v.class.flavor FROM WhiteboardViewers.text => e _ FetchWBItem[name, note]; WhiteboardViewers.icon => e _ FetchWBItem[name, icon]; ENDCASE => e _ NIL }; StoreEntity: PUBLIC PROC[v: Viewer, name: ROPE] = { ViewerOps.AddProp[v, $Entity, name ] }; <> GetIconProps: PUBLIC ENTRY PROC[wbIcon: Viewer] RETURNS[ name: ROPE, type: ATOM ] = { ENABLE UNWIND => NULL; DoGet: PROC[] = { iconEntity: WBItem = FetchEntity[wbIcon]; [name, type] _ WhiteboardDBPrivate.GetIconProps[iconEntity] }; name _ NARROW[ViewerOps.FetchProp[wbIcon, $EntityName]]; IF name # NIL THEN type _ NARROW[ViewerOps.FetchProp[wbIcon, $IconType]] ELSE CarefullyApply[DoGet] }; GetToolArgument: PUBLIC ENTRY PROC[wbIcon: Viewer] RETURNS[ argument: ROPE] = { ENABLE UNWIND => NULL; DoGet: PROC[] = { iconEntity: WBIcon = FetchEntity[wbIcon]; <> IF iconEntity = NIL THEN {argument _ NIL; RETURN}; argument _ WhiteboardDBPrivate.GetToolArgument[iconEntity] }; argument _ NARROW[ViewerOps.FetchProp[wbIcon, $IconArgument]]; IF argument = NIL THEN CarefullyApply[DoGet]; ViewerOps.AddProp[wbIcon, $IconArgument, argument] }; <> EstablishWhiteboardDB: PUBLIC ENTRY PROC[DBFile: ROPE _ NIL] = { <> ENABLE UNWIND => NULL; IF DBFile = NIL THEN { IF WhiteboardDBPrivate.WBSegment # NIL THEN { <> InternalResetViewers[destroy: FALSE]; stopped _ FALSE } ELSE { stopped _ TRUE; WBError[$NoDatabaseOpen] }; RETURN }; <> IF NOT Rope.Equal[DBFile, WhiteboardDBPrivate.WBSegment, FALSE] THEN InternalResetViewers[TRUE]; IF WhiteboardDBPrivate.Initialize[DBFile].serverDown THEN ERROR WBError[$ServerDown]; stopped _ FALSE }; InternalResetViewers: INTERNAL PROC[destroy: BOOL] = { IF destroy THEN { DestroyProc: ViewerOps.EnumProc = { IF v.class.flavor = $Whiteboard THEN ViewerOps.DestroyViewer[v]; RETURN[TRUE] }; aborted: BOOL _ FALSE; <> WhiteboardDBPrivate.CloseTransaction[]; <> ViewerOps.EnumerateViewers[enum: DestroyProc] } ELSE { ResetProc: INTERNAL ViewerOps.EnumProc = { <> IF v.class.flavor = $Whiteboard AND NOT v.newVersion AND NOT v.destroyed THEN InternalReset[v]; RETURN[TRUE] }; doingRestart _ TRUE; ViewerOps.EnumerateViewers[enum: ResetProc]; <> doingRestart _ FALSE } }; CarefullyApply: INTERNAL PROC [proc: PROC[]] ~ { outcome: {aborted, failure, error, normal} _ normal; BEGIN ENABLE BEGIN DB.Error => {outcome _ error; CONTINUE}; DB.Failure => {outcome _ failure; CONTINUE}; DB.Aborted => {outcome _ aborted; CONTINUE}; END; IF stopped THEN RETURN; -- don't bother trying to do the operation activity _ TRUE; BEGIN ENABLE DB.Aborted => { outcome _ aborted; CONTINUE }; WhiteboardDBPrivate.OpenTransaction[]; proc[] END; IF outcome = normal THEN RETURN; -- no aborted occurred WhiteboardDBPrivate.AbortTransaction[]; WhiteboardDBPrivate.OpenTransaction[]; proc[]; -- don't bother trying to restart here -- END; IF outcome = normal THEN RETURN; WhiteboardDBPrivate.AbortTransaction[]; IF outcome = error THEN ERROR WBError[$InternalError]; IF outcome = failure THEN ERROR WBError[$ServerDown]; ERROR WBError[$TransactionAbort] }; CloseWB: ENTRY Booting.CheckpointProc = { ENABLE UNWIND => NULL; WhiteboardDBPrivate.CloseTransaction[]; stopped _ TRUE }; OpenUp: Booting.RollbackProc = { DB.Initialize[nCachePages: 256] }; NewUserReset: ENTRY UserCredentials.CredentialsChangeProc = { ENABLE UNWIND => NULL; DestroyProc: ViewerOps.EnumProc = { IF v.class.flavor = $Whiteboard THEN ViewerOps.DestroyViewer[v]; RETURN[TRUE] }; <> WhiteboardDBPrivate.CloseTransaction[]; <> ViewerOps.EnumerateViewers[enum: DestroyProc] }; <> TRUSTED { Process.Detach[FORK WatchDBActivity[]]; Booting.RegisterProcs[c: CloseWB, r: OpenUp] } END...