2011年9月25日 星期日

Lala's Program Note 實作記錄: 31. 匯入預設的 Core Data 資料到專案裡

since: 2011/09/25
update: 2011/09/26


A. 準備好已儲存資料的 Core Data sqlite 檔案.
   方法 1:
   直接在模擬器的 UI 操作介面中先建好資料, 然後到 iPhone Simulator Directory 裡,
   將 app Documents 目錄下的 xxx.sqlite 檔 copy 出來.
   說明: 可先建立 iPhone Simulator Directory 的 symbolic link, 方便使用.

   方法 2:
   在實機測試時, 輸入想要預設儲存的資料, 然後配合:
   利用 iTunes 備份與回復 Core Data 資料, 將 xxx.sqlite 檔 copy 出來.

   方法 3: (未試過)
   藉由 方法 1方法 2, copy 出不含資料的空白資料庫, 然後利用:
   SQLite database browser 工具, 匯入 CSV 格式的資料到 xxx.sqlite 檔.

--------------------------------------------------------------------------------------------------

B. 將準備好的 xxx.sql 檔案加到專案裡:
   先新增一個 Resources 的 Group, 然後再將 xxx.sqlite 檔加進來.

--------------------------------------------------------------------------------------------------

C. 開啟 Lala_s_Program_NoteAppDelegate.m 檔, 修改如下:

/**
 Returns the persistent store coordinator for the application.
 If the coordinator doesn't already exist, it is created and the application's store added to it.
 */
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (__persistentStoreCoordinator != nil)
    {
        return __persistentStoreCoordinator;
    }
   
    /*
    //@add: import prepopulated sqlite data to a coredata db
     */
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

    NSString *documentsDirectory = [paths objectAtIndex:0];
   
    // a. 要存放預設資料的位置, 即 app 在執行時會讀取 .sql 的地方
    NSString *writableDBPath = [documentsDirectory stringByAppendingPathComponent:@"Lala_s_Program_Note.sqlite"];
    //NSLog(@"writableDBPath %@", writableDBPath);
    /*
     writableDBPath
    
     Simulator: /Users/lanli/Library/Application Support/iPhone Simulator/4.3.2/Applications/8F85FBE9-9C82-4DCD-84D5-C68173657E4C/Documents/Lala_s_Program_Note.sqlite
    
     iDevice: /var/mobile/Applications/C3F2AB7F-A296-4E9D-96C9-554E9499E1D4/Documents/Lala_s_Program_Note.sqlite
     */
   
    // b. 一開始加到專案裡的 .sql 位置, 直接就在 app 的目錄下
    NSString *defaultStorePath = [[NSBundle mainBundle] pathForResource:@"Lala_s_Program_Note" ofType:@"sqlite"];
    //NSLog(@"defalultStorePath %@", defaultStorePath);
    /*
     defalultStorePath
    
     Simulator: /Users/lanli/Library/Application Support/iPhone Simulator/4.3.2/Applications/8F85FBE9-9C82-4DCD-84D5-C68173657E4C/Lala's Program Note.app/Lala_s_Program_Note.sqlite
    
     iDevice: /var/mobile/Applications/C3F2AB7F-A296-4E9D-96C9-554E9499E1D4/Lala's Program Note.app/Lala_s_Program_Note.sqlite
     */   
   
    // c. app 在執行時讀取 .sql 的 URL地方
    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Lala_s_Program_Note.sqlite"];
    //NSLog(@"store URL %@", storeURL);
    /*
     store URL
    
     Simulator: file://localhost/Users/lanli/Library/Application%20Support/iPhone%20Simulator/4.3.2/Applications/8F85FBE9-9C82-4DCD-84D5-C68173657E4C/Documents/Lala_s_Program_Note.sqlite
    
     iDevice: file://localhost/var/mobile/Applications/C3F2AB7F-A296-4E9D-96C9-554E9499E1D4/Documents/Lala_s_Program_Note.sqlite
     */
   
    // Put down default db if it doesn't already exist
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if (![fileManager fileExistsAtPath:writableDBPath]) {
        if (defaultStorePath) {
            [fileManager copyItemAtPath:defaultStorePath toPath:writableDBPath error:NULL];
        }
    }
   
    //@original:
    //NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Lala_s_Program_Note.sqlite"];
    NSError *error = nil;
    __persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error])
    {
        . . . .
        //NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        //abort();
        //@update
        [Common showCoreDataError];
    }   
   
    return __persistentStoreCoordinator;
}

--------------------------------------------------------------------------------------------------

D. 結果圖:

2011年9月24日 星期六

Lala's Program Note 實作記錄: 30. Error Handing: Validation Errors

since: 2011/09/24
update: 2011/12/05


A. 先在 data model 裡, 限制 NoteBook 的 name 屬性與 NoteArticle 的 title 屬性
    之字串長度限制. (說明: 最小皆設為 1; 最大皆設為 25)


--------------------------------------------------------------------------------------------------

B. 實作 Validation Error Handing
   1. 開啟 Common.h 檔案, 修改如下:
      //@add
      + (NSString *)validationErrorText:(NSError *)error;


   2. 開啟 Common.m 檔案, 修改如下:
//@add
#pragma mark -
#pragma mark Validation Error Handling
+ (NSString *)validationErrorText:(NSError *)error {
    // Create a string to hold all the error messages
    NSMutableString *errorText = [NSMutableString stringWithCapacity:100];
   
    // Determine whether we're dealing with a single error or multiples, and put them all in an array
    NSArray *errors = ([error code] == NSValidationMultipleErrorsError?
    [[error userInfo] objectForKey:NSDetailedErrorsKey]:[NSArray arrayWithObject:error]);
   
    // Iterate through the errors
    for (NSError *err in errors) {
        // Get the property that had a validation error
        NSString *propName = [[err userInfo] objectForKey:@"NSValidationErrorKey"];
        NSString *message = nil;
       
        // From an appropriate error message
        switch ([err code]) {
            // Nonoptional property with a nil value   
            //case NSValidationMissingMandatoryPropertyError:
            //    message = [NSString stringWithFormat:@"%@ required", propName];
            //    break;
               
            // Some string value is too short
            case NSValidationStringTooShortError:
                // name, title: must be at least 1 character
                if ([propName isEqualToString:@"name"] || [propName isEqualToString:@"title"]) {

                    message = [NSString stringWithFormat:@"%@ must be at least %d character", propName, 1];
                }
                break;
               
            // Some string value is too long   
            case NSValidationStringTooLongError:
                // name, title: can't be longer than 25 characters
                if ([propName isEqualToString:@"name"] || [propName isEqualToString:@"title"]) {

                    message = [NSString stringWithFormat:@"%@ can't be longer than %d characters", propName, 25];
                }
                break;   
               
            default:
                message = @"Unknown error. Press Home button to halt.";
                break;
        }
       
        // Separate the error message with line feeds
        if ([errorText length] > 0) {
            [errorText appendString:@"\n"];
        }
       
        [errorText appendString:message];
    }
   
    return errorText;
}     

--------------------------------------------------------------------------------------------------

C. 新增 / 編輯 NoteBook 的 Validation 錯誤處理
   1. 開啟  NoteBookViewController.h 檔案, 修改如下:
//- (void)saveContext;
//@update: it will return error message
- (NSString *)saveContext;


//- (void)insertNoteBookWithName:(NSString *)name subName:(NSString *)subName;
//@update: When we added an object, if save failure, we can delete it by keeping a pointer to it
- (NSManagedObject *)insertNoteBookWithName:(NSString *)name subName:(NSString *)subName;


--------------------------------------------------------------------------------------------------

   2. 開啟  NoteBookViewController.m 檔案, 修改如下:
//- (void)saveContext {

//@update: it will return error message
- (NSString *)saveContext {

    NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];
    NSError *error = nil;
    //@add
    NSString *errorText = nil;
   
    if (![context save:&error]) {
        //NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        //abort();
        //@add
        errorText = [Common validationErrorText:error];
    }
   
    //@add
    return errorText;
}

--------------------------------------------------------------------------------------------------

//- (void)insertNoteBookWithName:(NSString *)name subName:(NSString *)subName {
//@update: When we added an object, if save failure, we can delete it by keeping a pointer to it
- (NSManagedObject *)insertNoteBookWithName:(NSString *)name subName:(NSString *)subName {
   
    NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];
    NSEntityDescription *entity = [[fetchedResultsController fetchRequest] entity];
    NSManagedObject *newNoteBook = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
   
    [newNoteBook setValue:name forKey:@"name"];
    [newNoteBook setValue:subName forKey:@"subName"];
   
     //@update
     return newNoteBook;

    //@update: handled in NoteBook.m
    //[newNoteBook setValue:[NSDate date] forKey:@"dateCreated"];
    //[newNoteBook setValue:[NSDate date] forKey:@"dateUpdate"];

    // Save the context
    //[self saveContext];
}

--------------------------------------------------------------------------------------------------

   3. 開啟  ManageNBViewController.m 檔案, 修改如下:
//@add

- (IBAction)save:(id)sender {
    //@add: trim NSString
    nameField.text = [nameField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];

    subNameField.text = [subNameField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
   
    //@add
    NSString *errorText = nil;
   
    //@add: Create variables to store pre-change state, so we can back out of if validation errors occur
    NSManagedObject *tempNoteBook = nil;
    NSString *tempName = nil;
    NSString *tempSubName = nil;
    NSDate *tempUpdateDate = nil;
   
   
    if (parenetController != nil) {
   
        /* editing */
        if (noteBook != nil) {
            //@add: User is editing an existing noteBook. Store its current values
            tempName = [NSString stringWithString:[noteBook valueForKey:@"name"]];
            tempSubName = [NSString stringWithString:[noteBook valueForKey:@"subName"]];
            tempUpdateDate = [noteBook valueForKey:@"dateUpdate"];
           
            // Update with the new values
            [noteBook setValue:nameField.text forKey:@"name"];
            [noteBook setValue:subNameField.text forKey:@"subName"];
            //now date
            [noteBook setValue:[NSDate date] forKey:@"dateUpdate"];
           
            // Save the context
            //[parenetController saveContext];
        }
        /* creating a new noteBook */
        else {
            //[parenetController insertNoteBookWithName:nameField.text subName:subNameField.text];
            //@update: User is adding a new noteBook. Create the new managed object but keep a pointer to it
            tempNoteBook = [parenetController insertNoteBookWithName:nameField.text subName:subNameField.text];
        }
       
        //@update: Save the context and gather any validation errors
        errorText = [parenetController saveContext];
    }
   
    //@add
    if (errorText != nil) {
        //@add: Validation error occurred. Show an alert
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error!" message:errorText delegate:nil cancelButtonTitle:nil otherButtonTitles:@"OK", nil];
       
        [alert show];
        [alert release];
       
        // Because we had errors and the context didn't save, undo any changes this method made
        if (tempNoteBook != nil) {
            //@add: We added an object, so delete it
            [[parenetController.fetchedResultsController managedObjectContext] deleteObject:tempNoteBook];
        }
        else {
            //@add: We edited an object, so restore it to how it was
            [noteBook setValue:tempName forKey:@"name"];
            [noteBook setValue:tempSubName forKey:@"subName"];
            [noteBook setValue:tempUpdateDate forKey:@"dateUpdate"];
        }
    }
    else {
        //@add: Successful save! Dismiss the modal only on success
        [self dismissModalViewControllerAnimated:YES];
    }
   
    //[self dismissModalViewControllerAnimated:YES];
}

--------------------------------------------------------------------------------------------------

D. 新增 / 編輯 Article 的 Validation 錯誤處理
   1. 開啟  ArticleListViewController.h 檔案, 修改如下:
//- (void)saveContext;

//@update: it will return error message
- (NSString *)saveContext;

//- (void)insertArticleWithTitle:(NSString *)title subTitle:(NSString *)subTitle content:(NSString *)content;
//@update: When we added an object, if save failure, we can delete it by keeping a pointer to it
- (NSManagedObject *)insertArticleWithTitle:(NSString *)title subTitle:(NSString *)subTitle content:(NSString *)content;

--------------------------------------------------------------------------------------------------

   2. 開啟  ArticleListViewController.m 檔案, 修改如下:
//- (void)saveContext {

//@update: it will return error message
- (NSString *)saveContext {
    NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];
   
    NSError *error = nil;
    //@add
    NSString *errorText = nil;
   
    if (![context save:&error]) {
        //NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        //abort();
        //@add
        errorText = [Common validationErrorText:error];
    }
   
    //@add
    return errorText;
}

--------------------------------------------------------------------------------------------------

//- (void)insertArticleWithTitle:(NSString *)title subTitle:(NSString *)subTitle content:(NSString *)content { 
//@update: When we added an object, if save failure, we can delete it by keeping a pointer to it
- (NSManagedObject *)insertArticleWithTitle:(NSString *)title subTitle:(NSString *)subTitle content:(NSString *)content { 
   
    NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];
   
    NSEntityDescription *entity = [[fetchedResultsController fetchRequest] entity];
   
    NSManagedObject *newArticle = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
   
    [newArticle setValue:title forKey:@"title"];
    [newArticle setValue:subTitle forKey:@"subTitle"];
    [newArticle setValue:content forKey:@"content"];
   
    //@update: use NSSet to handle to-many relationship
    NSMutableSet *articles = [self.noteBook mutableSetValueForKey:@"articles"];
    [articles addObject:newArticle]; 
   
    //@add
    return newArticle;
   
    //@update: handled in NoteArticle.m
    //Date
    //[newArticle setValue:[NSDate date] forKey:@"dateCreated"];
    //[newArticle setValue:[NSDate date] forKey:@"dateUpdate"];
   
    //[self.noteBook setValue:newArticle forKey:@"articles"];
    // 'NSInvalidArgumentException', reason: 'Unacceptable type of value for to-many relationship: property = "articles"; desired type = NSSet; given type = NSManagedObject;
   
    //@use NSSet to handle to-many relationship
    /*
    NSMutableSet *articles = [self.noteBook mutableSetValueForKey:@"articles"];
    [articles addObject:newArticle];   
   
    // Save the context
    [self saveContext];
     */
}

--------------------------------------------------------------------------------------------------

   3. 開啟  ManageArticleViewController.m 檔案, 修改如下:
//@add

- (IBAction)save:(id)sender {
    //@add: trim NSString
    titleField.text = [titleField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
   
    subTitleField.text = [subTitleField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
   
    contentText.text = [contentText.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
   
    //@update
    NSString *errorText = nil;   
   
    //@add: Create variables to store pre-change state, so we can back out of if validation errors occur
    NSManagedObject *tempArticle = nil;
    NSString *tempTitle = nil;
    NSString *tempSubTitle = nil;
    NSString *tempContent = nil;
    NSDate *tempUpdateDate = nil;
   

    if (parenetController != nil) {

        /* editing */
        if (article != nil) {
            //@add: User is editing an existing article. Store its current values
            tempTitle = [NSString stringWithString:[article valueForKey:@"title"]];
            tempSubTitle = [NSString stringWithString:[article valueForKey:@"subTitle"]];
            tempContent = [NSString stringWithString:[article valueForKey:@"content"]];
            tempUpdateDate = [article valueForKey:@"dateUpdate"];
           
            // Update with the new values
            [article setValue:titleField.text forKey:@"title"];
            [article setValue:subTitleField.text forKey:@"subTitle"];
            [article setValue:contentText.text forKey:@"content"];
           
            //now date
            [article setValue:[NSDate date] forKey:@"dateUpdate"];
           
            // Save the context
            //[parenetController saveContext];
        }
        /* creating a new article */
        else {
            //[parenetController insertArticleWithTitle:titleField.text subTitle:subTitleField.text content:contentText.text];
            //@add: User is adding a new article. Create the new managed object but keep a pointer to it
            tempArticle = [parenetController insertArticleWithTitle:titleField.text subTitle:subTitleField.text content:contentText.text];
        }
       
        //@update: Save the context and gather any validation errors
        errorText = [parenetController saveContext];
    }
    //@add
    if (errorText != nil) {
        //@add: Validation error occurred. Show an alert
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error!" message:errorText delegate:nil cancelButtonTitle:nil otherButtonTitles:@"OK", nil];
       
        [alert show];
        [alert release];
       
        //@add: Because we had errors and the context didn't save, undo any changes this method made
        if (tempArticle != nil) {
            //@add: We added an object, so delete it
            [[parenetController.fetchedResultsController managedObjectContext] deleteObject:tempArticle];
        }
        else {
            //@add: We edited an object, so restore it to how it was
            [article setValue:tempTitle forKey:@"title"];
            [article setValue:tempSubTitle forKey:@"subTitle"];
            [article setValue:tempContent forKey:@"content"];
            [article setValue:tempUpdateDate forKey:@"dateUpdate"];
        }
       
    }
    else {
        //@add: Successful save! Dismiss the modal only on success
        [self dismissModalViewControllerAnimated:YES];
    }
   
    //[self dismissModalViewControllerAnimated:YES];
}

--------------------------------------------------------------------------------------------------

E. 結果如下:




Lala's Program Note 實作記錄: 29. Error Handing: Core Data Operational

since: 2011/09/24
update: 2011/09/24


A. 新增一個共用的類別, 來處理共同的事情.
   1. 專案目錄 > New File >
   2. iOS > Cocoa Touch > Objective-C class > Next >
   3. Subclass 選擇: NSObject > Next
   4. Save As: Common.m

----------------------------------------------------------------------------------------------------------

B. 開啟 Common.h 檔案修改如下:
//@add
+ (void)showCoreDataError;

----------------------------------------------------------------------------------------------------------

C. 開啟 Common.m 檔案修改如下:
//@add
+ (void)showCoreDataError {
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error!" message:@"Lala's Program Note can't continue.\nPress the Home button to close it." delegate:nil cancelButtonTitle:nil otherButtonTitles:@"OK", nil];
   
    [alert show];
    [alert release];
}

----------------------------------------------------------------------------------------------------------

D. 套用到 AppDelegate 裡:
   開啟 Lala_s_Program_NoteAppDelegate.m 檔案, 修改如下:
   //@add
   #import "Common.h"
      . . . .
- (void)saveContext
{
    NSError *error = nil;
    NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
    if (managedObjectContext != nil)
    {
        if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error])
        {
            . . . .
            //NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            //abort();
            //@update
            [Common showCoreDataError];
        }
    }
}

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    . . . .
    if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error])
    {
        . . . .
        //NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        //abort();
        //@update
        [Common showCoreDataError];
    }   
   
    return __persistentStoreCoordinator;
}

----------------------------------------------------------------------------------------------------------

E. 套用到 SearchViewController 裡:
   開啟 SearchViewController.m 檔案, 修改如下:
   //@add
   #import "Common.h"

//@add
- (void) searchBarSearchButtonClicked:(UISearchBar *)theSearchBar
{
    . . . .
    if (![[self fetchedResultsController] performFetch:&error])
    {
        // Handle error
        //NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        //exit(-1);  // Fail
        //@add
        [Common showCoreDataError];
    }
    . . . .
}

----------------------------------------------------------------------------------------------------------

F. 套用到 FavoriteViewController 裡:
   開啟 FavoriteViewController.m 檔案, 修改如下:
   //@add
   #import "Common.h"

//@add
- (void)saveContext {
    NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];
   
    NSError *error = nil;
    if (![context save:&error]) {
        //NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        //abort();
        //@add
        [Common showCoreDataError];
    }
}


// Do it in the accessor for the fetchedResultsController member
- (NSFetchedResultsController *)fetchedResultsController {
    . . . .
    // Fetch the results into the fetched results controller
    NSError *error = nil;
    if (![[self fetchedResultsController] performFetch:&error]) {
        //NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        //abort();
        //@add
        [Common showCoreDataError];
    }
   
    return fetchedResultsController;
}


----------------------------------------------------------------------------------------------------------

G. 套用到 NoteBookViewController 裡:
   開啟 NoteBookViewController.m 檔案, 修改如下:
   //@add
   #import "Common.h"

// Creating the Fetched Results Controller:
// Do it in the accessor for the fetchedResultsController member
- (NSFetchedResultsController *)fetchedResultsController {
    . . . .
    // Fetch the results into the fetched results controller
    NSError *error = nil;
    if (![[self fetchedResultsController] performFetch:&error]) {
        //NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        //abort();
        //@add
        [Common showCoreDataError];
    }
   
    return fetchedResultsController;
}

----------------------------------------------------------------------------------------------------------

H. 套用到 ArticleListViewController 裡:
   開啟 ArticleListViewController.m 檔案, 修改如下:
   //@add
   #import "Common.h"

// Creating the Fetched Results Controller:
// Do it in the accessor for the fetchedResultsController member
- (NSFetchedResultsController *)fetchedResultsController {
    . . . .
    // Fetch the results into the fetched results controller
    NSError *error = nil;
    if (![[self fetchedResultsController] performFetch:&error]) {
        //NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        //abort();
        //@add
        [Common showCoreDataError];
    }
   
    return fetchedResultsController;
}

----------------------------------------------------------------------------------------------------------

I. 測試:
   1. 先執行一次 App, 關閉後到 data model 裡, 對 NoteBook entity 新增一個 attribute,
       然後再執行 App, 因為 data model 已經不相符合了, persistent store coordinator
       就無法開啓 data store 而發生 error 了.

   2. 結果圖:

2011年9月23日 星期五

Lala's Program Note 實作記錄: 28. Core Data Custom Validation

since: 2011/09/23
update: 2011/09/24

參考: Core Data Programming Guide: Managed Object Validation

備註: 已不採用此方式, 僅供參考.
         目前使用: 此方式

A. 說明:

   1. 需要先從 data model 產生出繼承自 NSManagedObject 的類別.
       這已在 Lala's Program Note 實作記錄: 20. search criteria 中的 B. 建立用來表現
       managed object 的客製物件: 建好了 NoteBook entity 與 NoteArticle entity 的類別.

----------------------------------------------------------------------------------------------------------

   2. Property-Level Validation:
       以下三種自行覆寫的方法, 由於 (id *)ioValue / (id *)value 是一個物件參照的指標,
       允許對輸入的值作更改, 因此在 Apple 的文件中也不建議使用, 因為可能造成
       記憶體管理的問題.
      - (BOOL)validate<AttributeName>:(id *)ioValue error:(NSError **)outError;
      - (BOOL)validateValue:(id *)value forKey:(NSString *)key error:(NSError **)error;
      - (BOOL)validateValue:(id *)ioValue forKeyPath:(NSString *)inKeyPath error:(NSError **)outError;
     
----------------------------------------------------------------------------------------------------------

   3. Inter-Property validation:
      a. 在此採用覆寫以下三個方法來做資料驗證的工作.(目前僅處理: InsertUpdate)
         - (BOOL)validateForInsert:(NSError **)error;
         - (BOOL)validateForUpdate:(NSError **)error;
         - (BOOL)validateForDelete:(NSError **)error;

      b. 在覆寫的方法內, 首先要先呼叫 superclass 的實作, 以取得合適的 Validation.
          ex: BOOL valid = [super validateForInsert:error];

      c. 當發現到有錯誤時, 可利用 errorFromOriginalError:error: 方法來組合所有的錯誤,
          以避免覆寫掉 superclass validation 裡的任何錯誤.
          Apple 預設的 errorFromOriginalError:error: 方法, 有需要也可以自行覆寫.

      d. 預設情況下, validation 要一直到對 managed object 執行 save 時, 才會發生作用,
         如果想提前呼叫的話, 可手動呼叫.
         ex:   NSError *validationError;
                 if (![newNoteBook validateForInsert:&validationError]) {
                     // Invalidate
                 } else {
                     // contine to save ....
                 }


B. 對 NoteBook 資料的 Validate:
   1. 開啟 NoteBook.h 檔案, 新增 method 如下:
//@add
- (BOOL)validateBookName:(NSError **)error;
- (NSError *)errorFromOriginalError:(NSError *)originalError error:(NSError *)secondError;


   2. 開啟 NoteBook.m 檔案, 新增 method 如下
//@add
- (BOOL)validateForInsert:(NSError **)error {
    BOOL superValid = [super validateForInsert:error];
    BOOL bookNameValid = [self validateBookName:error];
   
    return (superValid && bookNameValid);
}

----------------------------------------------------------------------------------------------------------

//@add
- (BOOL)validateForUpdate:(NSError **)error {
    BOOL superValid = [super validateForUpdate:error];
    BOOL bookNameValid = [self validateBookName:error];
   
    return (superValid && bookNameValid);
}

----------------------------------------------------------------------------------------------------------

//@add
- (BOOL)validateBookName:(NSError **)error
{
    BOOL valid = YES;
    // p.s. name is NoteBook model's attribute
    if (self.name != nil) {
        if (([self.name length] < 1) || ([self.name length] > 20)) {
            valid = NO;
            if (error != NULL) {
                NSBundle *myBundle = [NSBundle bundleForClass:[self class]];

                NSString *bookNameErrorString = [myBundle localizedStringForKey:@"StringLengthError" value:@"Book name length is too long or 0." table:@"BookNameErrorStrings"];
               
                NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];

                [userInfo setObject:bookNameErrorString forKey:NSLocalizedFailureReasonErrorKey];

                [userInfo setObject:self forKey:NSValidationObjectErrorKey];
               
                NSError *bookNameError = [NSError errorWithDomain:@"NoteBook" code:NSManagedObjectValidationError userInfo:userInfo];
               
                // if there was no previous error, return the new error
                if (*error == nil) {
                    *error = bookNameError;
                }
                // if there was a previous error, combine it with the existing one
                else {
                    *error = [self errorFromOriginalError:*error error:bookNameError];
                }
            }
        }
    }
   
    return valid;
}

----------------------------------------------------------------------------------------------------------

//@add
- (NSError *)errorFromOriginalError:(NSError *)originalError error:(NSError *)secondError
{
    NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
    NSMutableArray *errors = [NSMutableArray arrayWithObject:secondError];
   
    if ([originalError code] == NSValidationMultipleErrorsError) {
        [userInfo addEntriesFromDictionary:[originalError userInfo]];
        [errors addObjectsFromArray:[userInfo objectForKey:NSDetailedErrorsKey]];
    }
    else {
        [errors addObject:originalError];
    }
   
    [userInfo setObject:errors forKey:NSDetailedErrorsKey];
   
    return [NSError errorWithDomain:NSCocoaErrorDomain
                               code:NSValidationMultipleErrorsError
                           userInfo:userInfo];
}


   3. 當輸入的 NoteBook 名稱長度大於 20 或者沒有輸入時, Debug area 就會出現
      以下的錯誤訊息, 並且程式會終止(crash? 以後再作錯誤訊息的處理)
      "The operation couldn't be completed. Book name length is too long or 0."
       "NSLocalizedFailureReason = "Book name length is too long or 0.";


C. 對 NoteArticle 資料的 Validate:
   1. 開啟 NoteArticle.h 檔案, 新增 method 如下:
//@add
- (BOOL)validateArtitleTitle:(NSError **)error;
- (BOOL)validateArtitleContent:(NSError **)error;
- (NSError *)errorFromOriginalError:(NSError *)originalError error:(NSError *)secondError;

   2. 開啟 NoteArticle.m 檔案, 新增 method 如下
//@add
- (BOOL)validateForInsert:(NSError **)error {
    BOOL superValid = [super validateForInsert:error];
    BOOL artitleTitleValid = [self validateArtitleTitle:error];
    BOOL artitleContentValid = [self validateArtitleContent:error];
   
    return (superValid && artitleTitleValid && artitleContentValid);
}

----------------------------------------------------------------------------------------------------------

//@add
- (BOOL)validateForUpdate:(NSError **)error {
    BOOL superValid = [super validateForUpdate:error];
    BOOL artitleTitleValid = [self validateArtitleTitle:error];
    BOOL artitleContentValid = [self validateArtitleContent:error];
   
    return (superValid && artitleTitleValid && artitleContentValid);
}

----------------------------------------------------------------------------------------------------------

//@add
- (BOOL)validateArtitleTitle:(NSError **)error
{
    BOOL valid = YES;
   
    // p.s. title is NoteArtitle model's attribute
    if (self.title != nil) {
        if (([self.title length] < 1) || ([self.title length] > 20)) {
            valid = NO;
            if (error != NULL) {
                NSBundle *myBundle = [NSBundle bundleForClass:[self class]];
               
                NSString *artitleTitleErrorString = [myBundle localizedStringForKey:@"StringLengthError" value:@"Artitle title length is too long or 0." table:@"ArtitleTitleErrorStrings"];
               
                NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
               
                [userInfo setObject:artitleTitleErrorString forKey:NSLocalizedFailureReasonErrorKey];
               
                [userInfo setObject:self forKey:NSValidationObjectErrorKey];
               
                NSError *artitleTitleError = [NSError errorWithDomain:@"NoteArtitle" code:NSManagedObjectValidationError userInfo:userInfo];
               
                // if there was no previous error, return the new error
                if (*error == nil) {
                    *error = artitleTitleError;
                }
                // if there was a previous error, combine it with the existing one
                else {
                    *error = [self errorFromOriginalError:*error error:artitleTitleError];
                }
            }
        }
    }
   
    return valid;
}

----------------------------------------------------------------------------------------------------------

//@add
- (BOOL)validateArtitleContent:(NSError **)error
{
    BOOL valid = YES;
   
    // p.s. content is NoteArtitle model's attribute
    if (self.content != nil) {
        if ([self.content length] < 1) {
            valid = NO;
            if (error != NULL) {
                NSBundle *myBundle = [NSBundle bundleForClass:[self class]];
               
                NSString *artitleContentErrorString = [myBundle localizedStringForKey:@"StringLengthError" value:@"Artitle content length is 0." table:@"ArtitleContentErrorStrings"];
               
                NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
               
                [userInfo setObject:artitleContentErrorString forKey:NSLocalizedFailureReasonErrorKey];
               
                [userInfo setObject:self forKey:NSValidationObjectErrorKey];
               
                NSError *artitleContentError = [NSError errorWithDomain:@"NoteArtitle" code:NSManagedObjectValidationError userInfo:userInfo];
               
                // if there was no previous error, return the new error
                if (*error == nil) {
                    *error = artitleContentError;
                }
                // if there was a previous error, combine it with the existing one
                else {
                    *error = [self errorFromOriginalError:*error error:artitleContentError];
                }
            }
        }
    }
   
    return valid;
}

----------------------------------------------------------------------------------------------------------

//@add
- (NSError *)errorFromOriginalError:(NSError *)originalError error:(NSError *)secondError
{
    NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
    NSMutableArray *errors = [NSMutableArray arrayWithObject:secondError];
   
    if ([originalError code] == NSValidationMultipleErrorsError) {
        [userInfo addEntriesFromDictionary:[originalError userInfo]];
        [errors addObjectsFromArray:[userInfo objectForKey:NSDetailedErrorsKey]];
    }
    else {
        [errors addObject:originalError];
    }
   
    [userInfo setObject:errors forKey:NSDetailedErrorsKey];
   
    return [NSError errorWithDomain:NSCocoaErrorDomain
                               code:NSValidationMultipleErrorsError
                           userInfo:userInfo];
}

   3. 當輸入的 NoteArtitle 標題長度大於 20 或者沒有輸入時, Debug area 就會出現
      以下的錯誤訊息, 並且程式會終止(crash? 以後再作錯誤訊息的處理)
      "The operation couldn't be completed. Artitle title length is too long or 0."
       "NSLocalizedFailureReason = "Artitle title length is too long or 0.";

      而當沒有輸入 NoteArtitle 的內容時, Debug area 就會出現以下的錯誤訊息,
      並且程式會終止(crash? 以後再作錯誤訊息的處理)
      "The operation couldn't be completed. Artitle content length is 0."
       "NSLocalizedFailureReason = "Artitle content length is 0.";