现在的位置: 首页 > 综合 > 正文

NSUndoManager

2013年01月29日 ⁄ 综合 ⁄ 共 11208字 ⁄ 字号 评论关闭

使用NSUndoManaer, 我们可以给程序以一种优雅的风格添加undo功能. undo管理器跟踪管理一个对象的添加,编辑和删除.这些消息将会发送给undo管理器去做undo. 而当我们请求做undo操作时, undo管理器也会跟踪这些消息,这些消息会被记录用来做redo. 该机制使用两个NSInvocation对像堆栈来实现.

在这么早就讨论这个主题是相当沉重的.(有时候一说起undo.我的头就有点大.),不过因为undo和document架构关联,所以我们先来学习undo是怎么工作的.这样在下一章能更好理解document架构的工作流程.

NSInvocation
正如你所想, 应该有个对象能方便的封装一个消息[就是一个操作] - 包含selector, 接受对象,以及所有的参数 . NSInvocation对象就是这样的对象.

invocation一个非常方便的用途就是转发消息. 当一个对象接受到一个它没法响应的消息[没有实现该方法].message-sending系统不会马上抛出一个异常,它会先检查该对象是否实现了这个方法
- (void)forwardInvocation:(NSInvocation *)x
如果对象实现了该方法.那么这个消息就会被封包成对象NSInvocation-x.来调用forwardInvocation:方法

NSUndoManager是怎样工作的
假定一下用户打开一个新的RaiseMan document,并且做了3个编辑动作
.添加一条记录
.将记录的名字"New Employee" 修改为"Rex Fido"
.将raise改成20
当实现每一次修改时,controller将把一个要做undo的invocation添加到undo栈中.简单的说:"该修改的反向动作添加到undo栈中". 图9.1是在作了上面3个修改后的undo栈
[转载]第九章:NSUndoManager 如果这时候用户点击Undo菜单,那么第一个invocation将会抛出并调用.person的
raise会设置成0.如果用户再次点击Undo菜单,那么person的name将会修改回"New Employee"

每一次从Undo栈弹出执行一项时,反向操作将会压入到redo栈中.所以,当执行了上面说的两个undo动作后,undo和redo栈将会是这样的如图9.2
[转载]第九章:NSUndoManager
undo manager是非常智能的,当用户做编辑动作时,undo invocation将加入到undo栈中,当用户undo编辑时,undo invocation将加入到redo 栈. 而当用户redo编辑时, undo invocation又加入到undo栈中. 这样操作都是自动完成的. 我们的任务仅仅是提供给undo manager需要做反向操作的invocation.

现在假设我们编写一个方法 makeItHotter, 它的反向操作方法为 makeItColder. 看看是如何实现undo的
- (void)makeItHotter
{
    temperature = temperature + 10;
    [[undoManager prepareWithInvocationTarget:self] makeItColder];
    [self showTheChangesToTheTemperature];
}

你可能猜到了, prepareWithInvocationTarget: 记录了target [self].并且返回undo manager 它自己. undo manager重载了forwardInvocation: 把invocation-makeItColder: 加入到 undo栈中

所以,我们还有实现方法makeItColder
- (void)makeItColder
{
    temperature = temperature - 10;
    [[undoManager prepareWithInvocationTarget:self] makeItHotter];
    [self showTheChangesToTheTemperature];
}
我们在undo manager中注册了反向操作. 在执行undo时,makeItColder将被执行,而它的反向makeItHotter将会添加到redo栈中

每个栈中的invocation会是聚合的. 默认的,当单一事件[做了一个操作]发生时加入到栈中的所有invocation将会是聚合在一起 [这里要理解什么是invocation, 简单来讲,它就是某个对象的某个方法. 所以当你做某个单一操作时,可能会涉及到多个对象,多个方法. 也就是多个invocation]. 所以,当用户的一个操作改变了多个对象时,  如果点击undo 菜单,那么所有的改变都会一次undo完成.

我们也可以来修改菜单 Undo 和Redo 标题. 比如使用Undo Insert来代替简单的Undo. 可以使用如下代码
[undoManager setActionName:@"Insert"];

那么,怎么得到一个undo manager呢?你可以直接创建. 不过注意,NSDocument对象已经有一个自己的undo manager [它也是自己创建的哈]


为RaiseMan添加Undo功能

为了使用户可以使用undo功能: undo点击 Add New Employess 和 Delete.以及undo 对person对象的修改. 我们必须给MyDocument添加代码

当我设计类时, 我会考虑为什么要定义一个成员变量? 一定是下面的4个目的之一
1. 简单的属性: 比如学生的名字. 它们一般会是数字或NSString,NSNumber,NSDate,NSData对象
2. 单一关系: 比如一个学生一定会有一个学校和他相关. 这和1比较像,只是它的类型是一个复杂对象.单一关系使用指针来实现: 学生对象有一个指向学校对象的指针
3. 有序的多元关系: 比如,每个播放列表会有一系列歌曲和它关联. 这些歌曲有特定的顺序. 这样的关系一般使用NSMutableArray来实现
4. 无序的多元关系: 比如,每个部门会有一些雇员,我们可以对雇员按某个方式来排序(比如按照姓氏),不过这样的顺序都不是本质上的顺序. 一般使用NSMutalbeSet来实现.

早些时候,我们讨论了怎样使用key-vaule coding来设置简单属性和单一关系
.当setting 或是 getting fido的值时, key-value coding使用accessor 方法.同样的我们可以为有序的多元关系和无序的多元关系创建accessor方法.

来看看,对象playlist有一个NSMutabelArray变量来存放Song对象.如果你使用key-value coding来操作这个array对象,你将调用mutableArrayVauleForKey: .  得到一个代理对象,这个代理对象表示那个array.
id arrayProxy = [playlist mutableArrayValueForKey:@"songs"];
int songCount = [arrayProxy count];
这个例子中.当调用 count方法时.代理对象将先看playlist对象有没有实现 countOfSongs方法.如果有,那么就会调用该方法并返回结果.如果没有, 那么会调用保存song的array的count 方法如图9.3. 注意.方法countOfSongs的命名不仅仅是因为编码习惯: key-vaule coding机制使用这样的名字来查找
[转载]第九章:NSUndoManager 
下面是几个例子
id arrayProxy = [playlist mutableArrayValueForKey:@"songs"];

int x = [arrayProxy count]; // is the same as
int x = [playlist countOfSongs]; // if countOfSongs exists

id y = [arrayProxy objectAtIndex:5] // is the same as
id y = [playlist objectInSongsAtIndex:5]; // if the method exists

[arrayProxy insertObject:p atIndex:4] // is the same as
[playlist insertObject:p inSongsAtIndex:4]; // if the method exists

[arrayProxy removeObjectAtIndex:3] // is the same as
[playlist removeObjectFromSongsAtIndex:3] // if the method exists

对于无序多元关系也是一样的如图9.4

[转载]第九章:NSUndoManager
id setProxy = [teacher mutableSetValueForKey:@"students"];

int x = [setProxy count]; // is the same as
int x = [teacher countOfStudents]; // if countOfStudents exists

[setProxy addObject:newStudent]; // is the same as
[teacher addStudentsObject:newStudent]; // if the method exists

[setProxy removeObject:expelledStudent]; // is the same as
[teacher removeStudentsObject:expelledStudent]; // if the method exists

因为我们绑定了array controller的contentArray和Mydocument对象的employees. 所以array controller将会使用key-vaule coding来添加和删除person对象. 我们可以使用这个机制来实现当添加person对象时添加unod invocation到undo栈. 给MyDocument,m添加如下方法
- (void)insertObject:(Person *)p inEmployeesAtIndex:(int)index
{
    NSLog(@"adding %@ to %@", p, employees);
    // Add the inverse of this operation to the undo stack
    NSUndoManager *undo = [self undoManager];
    [[undo prepareWithInvocationTarget:self]
                          removeObjectFromEmployeesAtIndex:index];
    if (![undo isUndoing]) {
        [undo setActionName:@"Insert Person"];
    }
    // Add the Person to the array
    [employees insertObject:p atIndex:index];
}

- (void)removeObjectFromEmployeesAtIndex:(int)index
{
    Person *p = [employees objectAtIndex:index];
    NSLog(@"removing %@ from %@", p, employees);
    // Add the inverse of this operation to the undo stack
    NSUndoManager *undo = [self undoManager];
    [[undo prepareWithInvocationTarget:self] insertObject:p
                                       inEmployeesAtIndex:index];
    if (![undo isUndoing]) {
        [undo setActionName:@"Delete Person"];
    }

    [employees removeObjectAtIndex:index];
}

当给NSArrayController添加或是删除Person对象时,这些方法会自动调用:例如, 当Create New 和Delete 按钮发送insert: 和 remove: 消息的时候

在MyDocument.h中声明
- (void)removeObjectFromEmployeesAtIndex:(int)index;
- (void)insertObject:(Person *)p inEmployeesAtIndex:(int)index;
由于使用了Person类.所以我们需要告知编译器. 在MyDocument.h中添加
#import <Cocoa/Cocoa.h>
@class Person;
同样,在MyDocument.m中导入Person.h
#import "Person.h"

好了,我们已经可以undo添加和删除了. 对于undo 编辑会有点复杂. 在搞定它前,先编译运行我们的程序.试试undo功能. 注意,redo功能也是可用的

Key-Vaule Observing
在第7章,我们讨论了key-vaule coding. 回忆一下,key-vaule coding是一种通过变量名字来读取和修改变量值的方法. 而key-vaule  observing是当这些改变发生时我们能得到通知.

为了实现undo 编辑,我们需要让document对象能够得到改变了Person对象expectedRaise和personName的通知. NSObject的一个方法可以用来注册这样的通知
- (void)addObserver:(NSObject *)observer
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(void *)context;

对象observer为要通知的对象, keyPath标识激活通知的改变. options定义了通知包含的内容选项,例如,是否包含改变前的值,是否包含改变后的值. context是一个随着通知一起发送的对象,可以包含任何信息.一般为NULL.

当一个改变发生, observer对象将收到下面的消息
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context;
observer会知道那个对象的那个key path改变了. change是一个dictionary,其中的内容会依据在注册是option指定的值. 可能包含改变前的值和(或)改变后的值. 而context指针就是注册是的context指针,通常情况下,忽略它.

Undo编辑

第一步是将document对象注册观察它自己的person对象改变.在MyDocument.m中添加如下方法
- (void)startObservingPerson:(Person *)person
{
    [person addObserver:self
             forKeyPath:@"personName"
                options:NSKeyValueObservingOptionOld
                context:NULL];

    [person addObserver:self
             forKeyPath:@"expectedRaise"
                options:NSKeyValueObservingOptionOld
                context:NULL];
}

- (void)stopObservingPerson:(Person *)person
{
    [person removeObserver:self forKeyPath:@"personName"];
    [person removeObserver:self forKeyPath:@"expectedRaise"];
}

在添加或删除Person对象是调用上面的方法
- (void)insertObject:(Person *)p inEmployeesAtIndex:(int)index
{
    // Add the inverse of this operation to the undo stack
    NSUndoManager *undo = [self undoManager];
    [[undo prepareWithInvocationTarget:self]
         removeObjectFromEmployeesAtIndex:index];
    if (![undo isUndoing]) {
        [undo setActionName:@"Insert Person"];
    }

    // Add the Person to the array
    [self startObservingPerson:p];
    [employees insertObject:p atIndex:index];
}

- (void)removeObjectFromEmployeesAtIndex:(int)index
{
    Person *p = [employees objectAtIndex:index];
    // Add the inverse of this operation to the undo stack
    NSUndoManager *undo = [self undoManager];
    [[undo prepareWithInvocationTarget:self] insertObject:p
                                       inEmployeesAtIndex:index];
    if (![undo isUndoing]) {
        [undo setActionName:@"Delete Person"];
    }
    [self stopObservingPerson:p];
    [employees removeObjectAtIndex:index];
}

- (void)setEmployees:(NSMutableArray *)a
{
    if (a == employees)
        return;

    for (Person *person in employees) {
        [self stopObservingPerson:person];
    }

    [a retain];
    [employees release];
    employees = a;
    for (Person *person in employees) {
        [self startObservingPerson:person];
    }
}

实现编辑修改方法
- (void)changeKeyPath:(NSString *)keyPath
             ofObject:(id)obj
              toValue:(id)newValue
{
    // setValue:forKeyPath: will cause the key-value observing method
    // to be called, which takes care of the undo stuff
    [obj setValue:newValue forKeyPath:keyPath];
}

实现当Person 对象编辑通知响应方法,
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    NSUndoManager *undo = [self undoManager];
    id oldValue = [change objectForKey:NSKeyValueChangeOldKey];

    // NSNull objects are used to represent nil in a dictionary
    if (oldValue == [NSNull null]) {
        oldValue = nil;
    }
    NSLog(@"oldValue = %@", oldValue);
    [[undo prepareWithInvocationTarget:self] changeKeyPath:keyPath
                                                  ofObject:object
                                                   toValue:oldValue];
    [undo setActionName:@"Edit"];
}

好了,现在编译运行程序, undo 和redo功能完全可以工作了.

注意到了吗? 一旦我们修改了document, 窗口标题栏上的红色关闭按钮会出现一个黑点来提示我们,这些改变没有被保存. 在下一个章节,我们来学习把它们保存为文件

添加后里面编辑
我们的程序看上去运行的很好,不过有些用户可能会抱怨"当我插入一条记录后,为什么我必须双击才能开始编辑?很明显的我一定会修改新增person的名字啊."

这会有些复杂,我打算提供所需的代码片段,首先,MyDocument.h中添加一个acton和两个成员变量
@interface MyDocument : NSDocument
{
    NSMutableArray *employees;
    IBOutlet NSTableView *tableView;
    IBOutlet NSArrayController *employeeController;
}
- (IBAction)createEmployee:(id)sender;
保存文件,(我们记住一定要保存.h文件.这样新加的action和outlet才能在Interface Builder中找到)在Interface Builder中Control-drag Add New Employee按钮到File's Owner(MyDocument对象). 设置action为createEmployee: 如图9.5
[转载]第九章:NSUndoManager

Control-click file's Owner,设置好outlet tableView 和 employeeController如图9.6
[转载]第九章:NSUndoManager 
在MyDocument.m中添加 createEmployee:方法
- (IBAction)createEmployee:(id)sender
{
    NSWindow *w = [tableView window];

    // Try to end any editing that is taking place
    BOOL editingEnded = [w makeFirstResponder:w];
    if (!editingEnded) {
        NSLog(@"Unable to end editing");
        return;
    }
    NSUndoManager *undo = [self undoManager];

    // Has an edit occurred already in this event?
    if ([undo groupingLevel]) {
        // Close the last group
        [undo endUndoGrouping];
        // Open a new group
        [undo beginUndoGrouping];
    }
    // Create the object
    Person *p = [employeeController newObject];

    // Add it to the content array of 'employeeController'
    [employeeController addObject:p];
    [p release];
    // Re-sort (in case the user has sorted a column)
    [employeeController rearrangeObjects];

    // Get the sorted array
    NSArray *a = [employeeController arrangedObjects];

    // Find the object just added
    int row = [a indexOfObjectIdenticalTo:p];
    NSLog(@"starting edit of %@ in row %d", p, row);

    // Begin the edit in the first column
    [tableView editColumn:0
                      row:row
                withEvent:nil
                   select:YES];
}
不能期望你能理解没一行代码.不过试着浏览这些方法,立即它们的基本原理. 编译运行程序吧.


思考: Windows和Undo Manager
可以把view编辑动作加入到undo manager.例如, NSTextView,可以把文字输入的动作加入到undo manager.使用Interface Builder来激活 如图9.7
[转载]第九章:NSUndoManager
那么text view是怎么知道使用哪一个undo manager呢?首先,它会询问delegate. NSTextView的delegate可以实现这个方法
- (NSUndoManager *)undoManagerForTextView:(NSTextView *)tv;
接下来,它会询问他的window. NSWindow有一个方法
- (NSUndoManager *)undoManager;
window的delegate可以实现一个方法来说明是否window可以提供undo manager
- (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window;

Undo/redo 菜单项反应了当前key window的undo manager状态(key window也就是大家说的active window. Cocoa 开发者叫它key 是因为用户的键盘输入事件由它接受)

抱歉!评论已关闭.