SDKでできること ~中級編~

SDKでできること ~中級編~

これからSDKで開発する人や既に開発している人のために、SDKでどんなことができるのか、サンプルを紹介していきます。

ASP.NETの知識があれば、FullWEB SDKの知識ゼロでも読み進められます。
(以下のサンプルコードはVisual Basicで書いています)

第5回 承認後の自動配信

FullWEBには承認フロー機能が標準で搭載されています。
FullWEBで承認が完了した直後に、関連するいくつかの処理を続けて行いたいことがあります。例えば業務連絡書のようなファイルを承認フローに回して最終承認者の承認が完了したら関係者に自動配信するというのはよくある業務シナリオの1つです。

例えばWORDで業務連絡書のファイルを作成し、分類フォルダ「業務連絡書」に登録します。

FullWEBの属性編集画面を起動して表題と配信先を設定しておきます。

承認依頼を出して上長の承認が完了したら、配信先に設定した部署の各ユーザにメール送信されていくという流れです。
以下は送信されたメール本文の例です。

今回はSDKを用いて、承認が完了した後に電子メールで関係者に配信する例を紹介します。


FullWEBでの設定

まず業務連絡書の分類ラベルを定義しておきます。表題と配信先の2つ属性項目を作っておきます。表題は配信メールのSubject、配信先は総務部や生産部など配信先の部署を想定しています。部署はFullWEBのユーザーグループとして作成されているとします。
複数配信先があるのを想定して多値入力の設定をチェックONにしておきます。こうすることによって、1つの属性項目にスペース区切りで複数の値を設定することができるようになります。

SDKで開発する内容

承認完了のイベントを待ち受けて、承認済になった業務連絡書ファイルから表題と配信先の属性値を取得します。それから配信先のユーザーグループに所属するユーザに対して電子メールを次々と送付します。

最初にVisual Studio で開発環境を整えます。
(FullWEB SDKマニュアルに掲載しています)

今回はVisual StudioでCnEventHandlerというクラスを追加してそこにコードを追加することにします。

このクラスにcnWebCoreTest.IWebCoreHandlerというインターフェースをImplementsするようにします。

Public Class CnEventHandler
    Implements cnWebCoreTest.IWebCoreEvent

    Public ReadOnly Property IsSupported(command As String) As Boolean
        Implements cnWebCoreTest.IFileFilter.IsSupported
        Get
        End Get
    End Property
    Public Function Filter(f As cnWebCoreTest.File, xParam As String) As String
        Implements cnWebCoreTest.IFileFilter.Filter

    End Function
    Public Function InitInstance(db As cnWebCoreTest.Database) As Boolean
        Implements cnWebCoreTest.IFileFilter.InitInstance

    End Function
    Public Sub TerminateInstance() Implements cnWebCoreTest.IFileFilter.TerminateInstance
    End Sub
End Class

IsSupported()がどのイベントを受け取るかを決めるプロパティです。各イベント発生時に呼び出され、Trueを返した場合に後述のFilter()メソッドが呼び出されます。

Filter()が各種イベント発生したときに呼ばれるメソッドです。

InitInstance()がこのクラスが初期化された時に呼ばれるメソッドです。(通常ログインした時です)
Databaseオブジェクトが渡ってきます。
※DatabaseオブジェクトはFullWEBの情報を取得するのに必要なオブジェクト

TerminateInstance()が解放時に呼ばれるメソッドです。(通常ログアウトした時です)
特別な処理がなければ、中身は空のままでも構いません。

準備したCnEventHandlerクラスをFullWEBのweb.configに以下のように設定してFullWEBに登録しておきます。
nameはdllファイル名、typeはクラス名を指定します。

    <webCoreEvent>
      <add name="WebApplication1.dll" type="CnWebApplication1.CnEventHandler" />
    </webCoreEvent>

こうすることによって、ユーザがFullWEBで承認作業を行ったときにCnEventHandlerクラスの各メソッドが自動的に呼び出されます。

これで準備ができましたので、実装していきます。
まずInitInsntance()を実装して、初期化された時にDatabaseオブジェクトを保持するようにします。

    Private m_db As cnWebCoreTest.Database

    ''' <summary>
    ''' 初期化処理
    ''' </summary>
    Public Function InitInstance(db As Database) As Boolean Implements IFileFilter.InitInstance
 
        m_db = db
        Return True
    End Function

IsSupported()を実装して承認完了イベントを受け取ることができるようにします。
引数にはイベント名が渡ってきます。”AfterApprove”が承認完了時のイベント名です。

    ''' <summary>
    ''' 各イベントを受け取るかどうかを返す
    ''' </summary>
    ''' <param name="command">各種イベント名</param>
    ''' <returns></returns>
    Public ReadOnly Property IsSupported(command As String) As Boolean Implements IFileFilter.IsSupported
        Get
            Select Case command.ToLower
                Case "afterapprove"
                    '   承認完了イベントを受け取る
                    Return True
                Case Else
                    '   それ以外は受け取らない
                    Return False
            End Select
        End Get
    End Property

Filter()を実装して、承認完了時の処理を書きます。
AfterApproveイベントが発生した時に後述のAfterApprove()メソッドを呼び出しています。

この時渡されるパラメータは各種承認情報が入っているXMLです。

    Public Function Filter(f As File, xParam As String) As String Implements IFileFilter.Filter
        Dim xp As XElement = XElement.Parse(xParam)
        Select Case xp.@command.ToLower
            Case "afterapprove"
                Return AfterApprove(xp)
            Case Else
                Return False
        End Select
    End Function

    Private Function AfterApprove(ByVal xp As XElement) As String
	・・・
    End Function

XMLの中には1つのファイルに対する承認依頼をユニークに区別するsdidという値が入っています。そこから承認されたファイルをユニークに区別するfidを取得します。
AfterApproveイベントでは最終承認完了だけでなく、差し戻し、承認完了(次の承認者へ)のタイミングでも発生します。XMLの中にはその種類が分かる文字列が入っているので、最終承認完了の時だけ後述のDistributeFile()メソッドを呼び出すようにします。

   Private Function AfterApprove(ByVal xp As XElement) As String
        For Each xt In xp.<ApprovalParam>.<Target>
            '   対象ファイルを取得
            Dim sdid As Integer = CInt(xt.@sdid)
            Dim fid As Integer = m_db.DbHelper.GetFidBySdid(sdid)
            '-- 承認処理
            Select Case (xt.@result)
                Case "done"
                    '   承認完了。配信処理へ
                    DistributeFile(fid)
                Case "continue"
                    '   承認(次の人へ)
                Case "rejected"
                    '   差し戻し
                Case Else
                    '   
            End Select
        Next
        Return xp.<ApprovalParam>(0).ToString
    End Function

    Private Sub DistributeFile(ByVal fid As Integer)
	・・・
    End Sub

DistributeFile()メソッドの中では、承認完了したファイルから表題と配信先の属性値を取得します。
配信先は部署で、FullWEBではユーザーグループとして扱う前提ですので、ユーザーグループ一覧の情報を取得して、所属ユーザのメールアドレス一覧を得ます。

最後に.NETの標準機能を使ってメール送信していけば処理完了です。

     Private Sub DistributeFile(ByVal fid As Integer)
        '   Fileオブジェクトの取得
        Dim f As cnWebCoreTest.File = m_db.GetFile(fid)
        If f.ContainsAttr("業務連絡書") Then
            '   業務連絡書ファイルではないので何もしない
            Return
        End If
        '   表題属性取得
        Dim alidTitle As Integer = m_db.GetALID("業務連絡書", "表題")
        Dim title As String = f.Attrs("業務連絡書").Item("k" & alidTitle.ToString).Value
        '   配信先属性取得
        Dim alidDist As Integer = m_db.GetALID("業務連絡書", "配信先")
        Dim distVal As String = f.Attrs("業務連絡書").Item("k" & alidDist.ToString).Value
        '   ユーザーグループの情報を取得する
        Dim dicUg As New Dictionary(Of String, Integer)
        Dim uglistXml As String = m_db.DbHelper.GetUserGroupsAll()
        Dim xUgList As XElement = XElement.Parse(uglistXml)
        For Each xUg As XElement In xUgList.<ug>
            If dicUg.ContainsKey(xUg.<value>.Value) = False Then
                dicUg.Add(xUg.<value>.Value, CInt(xUg.<id>.Value))
            End If
        Next
        '   メールアドレス一覧を取得する
        Dim emailAddressList As New List(Of String)
        For Each dist As String In distVal.Split(" "c) 
            If dicUg.ContainsKey(dist) = False Then
                '   配信先に設定された値がユーザーグループでない
                Continue For
            End If
            Dim emlist As List(Of String) = GetMailAddressList(dicUg.Item(dist))
            emailAddressList.AddRange(emlist.ToArray)
        Next
        SendMail(f, title, emailAddressList) '   .NET標準機能でメール送信
    End Sub

以下はユーザーグループ(部署)に所属するユーザ―のメールアドレス一覧を取得するメソッドです。
DistributeFile()メソッドから呼び出されています。

    ''' <param name="ugid">ユーザーグループを特定するugid</param>
    Private Function GetMailAddressList(ByVal ugid As Integer) As List(Of String)
        Dim ret As New List(Of String)
        Dim users As New Users(ugid, m_db)
        For i As Integer = 1 To users.Count
            Dim email As String = users.Item(i).eMail(0)
            If String.IsNullOrEmpty(email) = False Then ret.Add(email)
        Next
        Return ret
    End Function

DistributeFile()メソッドの最後にSendMail()という関数を呼び出していますが、.NET標準機能を使ってメール送信処理を実装していけば良いため、詳細は記述していません。

※メール本文に
https://hostname/FullWEB/File.aspx?fid=〇〇
というURLを入れておけば、メールを受け取ったユーザはこのリンクをクリックするだけで配信された業務連絡書の属性画面を直接起動することができるので便利です。

これで実装は完了です。
このようにFullWEBの機能とSDKを組み合わせることで、実現したいシナリオを柔軟に対処できます。

第6回 他システム連携(WEBアプリケーション)

第2, 3回でEXE形式の自動登録ツールを作成しました。
この自動登録ツールを使うと、他システムからは指定フォルダへファイルを配置するだけで自動でファイルが登録されます。
今回は他システムと連携するWEBアプリケーションを作成します。
これはFullWEB WEBアプリケーションとは別のWEBアプリケーションです。
下図の「独自WEBアプリケーション」の位置づけになります。

画像:独自WEBアプリケーションの位置づけ説明

Visual StudioでASP.NET Webアプリケーションプロジェクトを新規作成し、FullWEB SDKのモジュールを参照に追加します。(詳細はFullWEB SDKマニュアルに掲載しています)
今回作成するプログラムでは、ファイル番号を受け付けたら、該当する実ファイルデータをBASE64エンコードした文字列を返します。ただし、[公開設定]ラベルの[公開]属性項目(チェック型)がONになっているものを対象とします。
レスポンスデータは、BASE64エンコードした実ファイルデータを含むJSON文字列とします。
この機能を担当するaspxページをプロジェクトに追加します。
GetPublicFile.aspx という名前にしましょう。

GetPublicFile.aspxは、リクエストを受け付けたら、指定のユーザ/パスワードでログインします。
そして、リクエストパラメータのファイル番号からFileオブジェクトを取得し、[公開設定]ラベルの[公開]属性項目の値を調べます。
ここまでの流れは第1回から第3回で説明済みです。
コードは以下のようになります。

Const server As String = "localhost"                  ' 接続先
Const user As String = "admin"                        ' ユーザ名
Const password As String = ""                         ' パスワード

Dim lm As cnWebCoreTest.LoginManager = Nothing

Try
    ' リクエストからファイル番号を取得
    Dim fid = CInt(Request.Params("fid"))

    ' ログイン情報
    Dim xParam As XElement = <param
                                    mode="new"
                                    lcuser=<%= user %>
                                    lctime=<%= Now.ToString("yyyyMMddHHmmss") %>
                                    atime=<%= Now.ToString("yyyyMMddHHmmss") %>
                                    pc=""
                                    ipaddr=""
                                    datakey=""/>
    lm = cnWebCoreTest.LoginManager.CreateInstance(server, xParam)
    ' ログイン
    Dim err As String = lm.Connect(lm.UserName, password, True)

    If err <> "" Then
        ' ログインエラーなら終了
    End If

    Dim db As cnWebCoreTest.Database = lm.Database

    ' Fileオブジェクト取得
    Dim f As cnWebCoreTest.File = db.SafeGetFile(resData.fid)
    If f Is Nothing Then
        ' Fileオブジェクトが取得できなければ終了
    End If

    ' 属性値の確認
    If f.ContainsAttr("公開設定") = False Then
        ' 公開設定ラベルが付与されていなければ終了
    End If

    Dim alid As Integer = db.GetALID("公開設定", "公開")
    Dim alVal As String = f.Attrs("公開設定").Item("k" & alid.ToString).Value

    If alVal <> "1" Then
        ' 公開チェックボックスの値がONでなければ終了
    End If

Catch ex As Exception
    ' エラー
Finally
    ' 必ずログアウト
    If lm IsNot Nothing Then
        lm.Disconnect()
    End If
End Try

次は実ファイルを取得する処理です。
FileオブジェクトのCopy()メソッドを使います。
第一パラメータは、取り出し先フォルダのパス、第二パラメータは取り出し先に保存するファイル名です。
この取り出し先は独自WEBアプリケーションが動いているサーバのフォルダです。
マルチリクエストに対応できるように、ファイル名にはGUIDなどの重複しないファイル名を付ける必要があります。

' ファイルを一時フォルダに取り出す
Dim serverPath As String = "C:¥DAV"
Dim fileName As String = Guid.NewGuid.ToString & f.Extension
f.Copy(serverPath, fileName)

残りの作業は、ファイルデータをBASE64エンコードすることとJSON形式のレスポンスを返すことです。
これらは.NETのAPIでの実装になります。
BASE64エンコード文字列だけではなく、リクエストで受け付けたファイル番号、エラーがあった場合はエラーコードとエラーメッセージも返すようにしましょう。
コード全体は以下のようになります。
※server、user、password 変数の値はWeb.configから取得しています。
passwordは暗号化しています。Decryption() メソッドは適宜コーディングしてください。

■Web.config

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <appSettings>
    <add key="server" value="localhost"/>
    <add key="user" value="admin"/>
    <add key="password" value="aG9nZSANCg0K"/>
  </appSettings>
  <system.web>
    <compilation debug="true" strict="false" explicit="true" targetFramework="4.7.2" />
    <httpRuntime targetFramework="4.7.2" />
  </system.web>
  <system.codedom>
    <compilers>
      <compiler language="c#;cs;csharp"  />
      <compiler language="vb;vbs;visualbasic;vbscript" />
    </compilers>
  </system.codedom>
</configuration>

■GetPublicFile.aspx.vb

Private Structure ResponseData
    Property fid As Integer
    Property errorCode As Integer
    Property errorMessage As String
    Property fileName As String
    Property fileData As String
End Structure
			
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
    Dim resData As New ResponseData()
    Dim lm As cnWebCoreTest.LoginManager = Nothing
    Dim server As String = ConfigurationManager.AppSettings("server")    ' 接続先
    Dim user As String = ConfigurationManager.AppSettings("user")         ' ユーザ名
    Dim password As String = Decryption(ConfigurationManager.AppSettings("password"))    ' パスワード
			
    Try			
        ' リクエストからファイル番号を取得
        resData.fid = CInt(Request.Params("fid"))
			
        ' ログイン情報
        Dim xParam As XElement = <param
                                        mode="new"
                                        lcuser=<%= user %>
                                        lctime=<%= Now.ToString("yyyyMMddHHmmss") %>
                                        atime=<%= Now.ToString("yyyyMMddHHmmss") %>
                                        pc=""
                                        ipaddr=""
                                        datakey=""/>
        lm = cnWebCoreTest.LoginManager.CreateInstance(server, xParam)
        ' ログイン
        Dim err As String = lm.Connect(lm.UserName, password, True)
	
        If err <> "" Then
            ' ログインエラーなら終了
            With resData
                .errorCode = 1
                .errorMessage = err
            End With
            Exit Try
        End If
	
        Dim db As cnWebCoreTest.Database = lm.Database
	
        ' Fileオブジェクト取得
        Dim f As cnWebCoreTest.File = db.SafeGetFile(resData.fid)
        If f Is Nothing Then
            With resData
                .errorCode = 1
                .errorMessage = "ファイルが見つからない"
            End With
            Exit Try
        End If
	
        ' 属性値の確認
        If f.ContainsAttr("公開設定") = False Then
            With resData
                .errorCode = 1
                .errorMessage = "非公開ファイル"
            End With
            Exit Try
        End If
	
        Dim alid As Integer = db.GetALID("公開設定", "公開")
        Dim alVal As String = f.Attrs("公開設定").Item("k" & alid.ToString).Value

        If alVal <> "1" Then
            With resData
                .errorCode = 1
                .errorMessage = "非公開ファイル"
            End With
            Exit Try
        End If

        ' ファイルを一時フォルダに取り出す
        Dim serverPath As String = "C:¥DAV"
        Dim fileName As String = Guid.NewGuid.ToString & f.Extension
        f.Copy(serverPath, fileName)

        Dim workPath As String = IO.Path.Combine(serverPath, fileName)
			
        ' Base64文字列に変換
        Dim byteStream() As Byte = Nothing
        Dim base64str As String = ""
        Try
            Using fs As New IO.FileStream(workPath, IO.FileMode.Open, IO.FileAccess.Read)
                ReDim byteStream(fs.Length - 1)
                Dim readBytes As Long = fs.Read(byteStream, 0, fs.Length)
                fs.Close()
            End Using
            base64str = Convert.ToBase64String(byteStream)
        Finally
            ' 一時フォルダのファイルを削除
            If IO.File.Exists(workPath) Then
                IO.File.Delete(workPath)
            End If
        End Try
			
        resData.fileData = base64str
        resData.fileName = f.Name
    Catch ex As Exception
        With resData
            .errorCode = ex.HResult
            .errorMessage = ex.Message
        End With	
    Finally
        ' 必ずログアウト
        If lm IsNot Nothing Then
            lm.Disconnect()
        End If
    End Try
			
    ' JSON文字列を返す
    Response.ContentType = "application/json"
    Response.AddHeader("Access-Control-Allow-Origin", "*")    ' CORSを許可
    Response.AddHeader("Access-Control-Allow-Headers", "Origin, X - Requested -With, Content - Type, Accept")    ' CORSを許可
    Response.Clear()
    Response.Write(New JavaScriptSerializer().Serialize(resData))
    Response.Flush()
    Response.End()
End Sub
			
Private Function Decryption(s As String) As String
    ' 適宜、復号化を実施します。	
    Return s
End Function

完成したWEBアプリケーションをIISに登録したら、実際にアクセスしてみます。

以下のようなテストページを作って表示します。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>テストページ</title>
    <script type="text/javascript">
        function cnsdkExec() {
            var fid = document.getElementById('fid').value;
            var httpRequest = new XMLHttpRequest();
            if (!httpRequest) {
                return;
            }
            httpRequest.onreadystatechange = function () {
                if (httpRequest.readyState === XMLHttpRequest.DONE) {
                    if (httpRequest.status === 200) {
                        if (httpRequest.response.errorCode == 0) {
                            var a = document.createElement("a");
                            a.href = "data:application/octet-stream;base64," + httpRequest.response.fileData;
                            a.download = httpRequest.response.fileName;
                            a.click();
                        }
                    } else {
                        alert('リクエストに問題が発生しました');
                    }
                }
            }
            httpRequest.responseType = "json";
            httpRequest.open('Get', 'http://piglet/cmd211102/GetPublicFile.aspx/?fid=' + fid);
            httpRequest.send();
        }
    </script>
</head>
<body>
    <form action="GetPublicFile.aspx" method="post">
        <input id="fid" type="text" value="51393394" />
        <input type="button" value="Get File" onclick="cnsdkExec();" />
    </form>
</body>
</html>

ボタンをクリックするとファイルがダウンロードされます。

画像:ファイルダウンロード

第7回 属性更新時の独自チェック方法

属性画面で編集内容を保存する際に、項目間のチェックや変更前との比較チェックをしたい場合があります。
今回は、属性更新時のイベントをハンドルする処理を紹介します。

以下のようなシナリオで説明します。

  • プロジェクトの工程管理用のアイテムがあって、工程と進捗を管理します。
  • 各工程の担当者は進捗を更新します。
  • 進捗は増えることはあっても減ることはないものとしてプログラムでチェックします。
  • 進捗が100%に達すると、次の工程の担当者が進捗を更新していきます。

FullWEBでの設定

「工程管理」という分類ラベルを作成して以下の属性項目を定義します。

画像:属性項目-管理画面

SDKで開発する内容

最初にVisual Studio で開発環境を整えます。
(FullWEB SDKマニュアルに掲載しています)

今回はVisual StudioでCnEventHandlerというクラスを追加してそこにコードを追加することにします。

画像:Visual Stuido画面

このクラスにFullWEB.ICustomEventというインターフェースをImplementsするようにします。

Public Class CnEventHandler
    Implements FullWEB.ICustomEvent

    Public ReadOnly Property IsSupported(command As String) As Boolean Implements ICustomEvent.IsSupported
        Get
            Throw New NotImplementedException()
        End Get
    End Property

    Public Sub TerminateInstance() Implements ICustomEvent.TerminateInstance
        Throw New NotImplementedException()
    End Sub

    Public Function InitInstance(db As Database) As Boolean Implements ICustomEvent.InitInstance
        Throw New NotImplementedException()
    End Function

    Public Function PageEvent(srcPage As Page, xParam As XElement) As XElement Implements ICustomEvent.PageEvent
        Throw New NotImplementedException()
    End Function
End Class

IsSupported()がどのイベントを受け取るかを決めるプロパティです。
各イベント発生時に呼び出され、Trueを返した場合に後述の PageEvent()メソッドが呼び出されます。

PageEvent()が各種イベント発生したときに呼ばれるメソッドです。

InitInstance()がこのクラスが初期化された時に呼ばれるメソッドです。(通常ログインした時です)
第5回 承認後の自動配信と異なり引数パラメータのDatabaseオブジェクトはNULLの場合があります。

TerminateInstance()が解放時に呼ばれるメソッドです。(通常ログアウトした時です)
特別な処理がなければ、中身は空のままでも構いません。

準備したCnEventHandlerクラスをFullWEBのweb.configに以下のように設定してFullWEBに登録しておきます。
nameはdllファイル名、typeはクラス名を指定します。

<customEvent>
  <add name="cmd2112.dll" type="cmd2112.CnEventHandler" description=""/>
</customEvent>

こうすることによって、ユーザがFullWEBで属性編集画面の保存ボタンをクリックしたときにCnEventHandlerクラスの各メソッドが自動的に呼び出されます。

これで準備ができましたので、実装していきます。

最初に、工程と担当部署のマッピングを作っておきます。

' 工程とユーザグループのマッピング表
Private ReadOnly dicPhaseUsergroup As New Dictionary(Of String, String) From {
    {"設計", "設計部門"},
    {"製造", "製造部門"},
    {"テスト", "テスト部門"}
}

IsSupported()を実装して属性編集画面での保存前イベントおよび保存後イベントを受け取ることができるようにします。
引数にはイベント名が渡ってきます。”AttrBeforeUpdateAll”が保存前のイベント名、”AttrAfterUpdateAll”が保存後のイベント名です。

''' <summary>
''' 各イベントを受け取るかどうかを返す
''' </summary>
''' <param name="command">各種イベント名</param>
''' <returns></returns>
Public ReadOnly Property IsSupported(command As String) As Boolean Implements ICustomEvent.IsSupported
    Get
        Select Case command.ToLower
            Case "attrbeforeupdateall"
                ' 属性編集画面での保存前イベントを受け取る
                Return True
            Case "attrafterupdateall"
                ' 属性編集画面での保存後イベントを受け取る
                Return True
            Case Else
                ' それ以外は受け取らない
                Return False
        End Select
    End Get
End Property

PageEvent()を実装して、各イベント発生時の処理を書いていきます。

Databaseオブジェクトは以下のように取得します。

Dim db As cnWebCoreTest.Database = TryCast(srcPage.Session("objdb"), cnWebCoreTest.Database)

保存対象となるファイルは以下のようにして取得できます。
Filesオブジェクトという複数ファイルを表すオブジェクトです。

Dim fls As cnWebCoreTest.Files = CType(srcPage.Session(xParam.Value), cnWebCoreTest.Files)

個々のアイテム(Fileオブジェクト)はFilesオブジェクトのItemプロパティで取得します。

Public Function PageEvent(srcPage As Page, xParam As XElement) As XElement Implements ICustomEvent.PageEvent
    Dim db As cnWebCoreTest.Database = TryCast(srcPage.Session("objdb"), cnWebCoreTest.Database)
    Dim fls As cnWebCoreTest.Files = CType(srcPage.Session(xParam.Value), cnWebCoreTest.Files)

    For i As Integer = 1 To fls.Count
        Dim f As cnWebCoreTest.File = fls.Item(i)

        Select Case xParam.@command.ToLower
            Case "attrbeforeupdateall"
                ' 保存前処理
            Case "attrafterupdateall"
                ' 保存後処理
            Case Else
                ' NOP
        End Select
    Next

    Return xParam
End Function

まず、保存前の処理を作成していきます。

最初に、ユーザが現在の工程を担当するユーザグループに所属しているかどうかをチェックします。
ユーザ情報は、DatabaseオブジェクトのUserプロパティで取得できます。
UserオブジェクトのGroupプロパティでユーザグループの情報が取得できるのでユーザグループ名を確認します。

つづいて、進捗をチェックします。
編集前の値と比較して、進捗が減算されてないかチェックします。
Filesオブジェクトから取得したFileオブジェクトには、属性画面で変更された値のみが入っています。
変更前の値を確認するためには、Databaseオブジェクトを介して、保存されている情報を取得する必要があります。

' 変更前のオブジェクトを取得
Dim bf As cnWebCoreTest.File = db.SafeGetFile(f.Fid)

チェックの結果、問題があればエラーを返します。
引数パラメータで渡されるxParamの<param><errors>要素の子に<error>を追加します。

xParam.<errors>(0).Add(<error>エラーメッセージ</error>)

保存前処理全体は以下になります。
管理者ユーザは本制約を受けないようにしています。
ユーザが入力を誤って保存してしまった場合、管理者ユーザに元に戻してもらえるようにするためです。
また、工程はユーザが変更できないようにもチェックしています。

' 保存前処理
If f.ContainsAttr("工程管理") Then  ' 工程管理ラベルが付いたファイルの場合
    ' システム管理者ならばなにもしない
    If db.User.IsAdmin Then
        Continue For
    End If

    ' 変更前のオブジェクトを取得
    Dim bf As cnWebCoreTest.File = db.SafeGetFile(f.Fid)

    ' 工程は変更前オブジェクトから取得
    Dim phase As String = f.Attrs("工程管理").Item("k" & db.GetALID("工程管理", "工程")).Value
    Dim beforePhase As String = bf.Attrs("工程管理").Item("k" & db.GetALID("工程管理", "工程")).Value

    ' 工程の変更は不可
    If phase <> beforePhase Then
        xParam.<errors>(0).Add(<error>工程の変更はできません。</error>)
        Continue For
    End If

    ' 完了なら変更不可
    If phase = "完了" Then
        xParam.<errors>(0).Add(<error>完了しているため更新できません。</error>)
        Continue For
    End If

    ' 所属ユーザグループのチェック
    Dim enableEdit As Boolean = False
    For j As Integer = 1 To db.User.GroupCount
        If db.User.Group(j).Name = dicPhaseUsergroup(phase) Then
            enableEdit = True
            Exit For
        End If
    Next

    ' 変更後の進捗
    Dim progress As Integer = CInt(f.Attrs("工程管理").Item("k" &amp; db.GetALID("工程管理", "進捗")).Value)
    ' 変更前の進捗
    Dim beforeProgress As Integer = CInt(bf.Attrs("工程管理").Item("k" &amp; db.GetALID("工程管理", "進捗")).Value)

    ' 編集可能なユーザグループに所属していない場合、進捗の変更は不可
    If enableEdit = False AndAlso progress <> beforeProgress Then
        xParam.<errors>(0).Add(<error><%= phase %>工程のユーザグループに所属していないため更新できません。</error>)
        Continue For
    End If

    ' 進捗が減っていたらエラー
    If progress < beforeProgress Then
        xParam.<errors>(0).Add(<error>進捗を減らすことはできません。</error>)
        Continue For
    End If
End If

次に保存後の処理を作成していきます。

ここでは、進捗をチェックして100%に到達していたら次の工程に変更し、進捗を0に戻します。
ここでも同様に管理者ユーザは対象外にします。

' 保存後処理
If f.ContainsAttr("工程管理") Then
    ' システム管理者ならばなにもしない
    If db.User.IsAdmin Then
        Continue For
    End If

    Dim progress As Integer = CInt(f.Attrs("工程管理").Item("k" & db.GetALID("工程管理", "進捗")).Value)

    ' 進捗が100に達したら次工程に移る
    If progress = 100 Then
        Dim nextPhase As String = ""
        Select Case f.Attrs("工程管理").Item("k" & db.GetALID("工程管理", "工程")).Value
            Case "設計"
                nextPhase = "製造"
            Case "製造"
                nextPhase = "テスト"
            Case "テスト"
                nextPhase = "完了"
            Case Else
                ' NOP
                Continue For
        End Select

        f.Attrs("工程管理").Item("k" & db.GetALID("工程管理", "工程")).Value = nextPhase
        f.Attrs("工程管理").Item("k" & db.GetALID("工程管理", "進捗")).Value = 0

        ' データベースに反映
        f.UpdateAttr(True)
    End If
End If

動作確認

ユーザが進捗を減らして保存ボタンをクリックすると、下記のようにエラーメッセージが表示されます。
画像:エラーメッセージ