【UiPath】コード化されたワークフローで RPA Challenge を動かす その１ - 基本編

# はじめに
今回は 2023.10 にて正式リリースとなったコード化されたワークフローを使って RPA Challenge を自動化してみようと思います。

https://www.rpachallenge.com/?lang=ja

この記事は、[UiPath (produced with UiPath Friends) Advent Calendar 2023](https://qiita.com/advent-calendar/2023/uipath) の 13日目です。

https://qiita.com/advent-calendar/2023/uipath

# コード化されたワークフローとは
コード化されたオートメーションの一つで、アクティビティの代わりに C# でコードを書くことにより自動化処理を実現する仕組みです。Coded Workflow とも呼ばれています。

コード化されたワークフローの他にも、コード化されたテストケース、コードソースファイルがあります。それぞれ以下の役割となっています。

* **コード化されたワークフロー**：Xaml 形式のワークフローに相当
* **コード化されたテストケース**：Xaml 形式のテストケースに相当
* **コードソースファイル**：上記2つから呼び出すことができる処理を切り出したロジックを記載できる

https://docs.uipath.com/ja/studio/standalone/2023.10/user-guide/coded-automations-introduction

コード化されたオートメーションは UiPath Studio 2023.10 以降の **Windows またはクロスプラットフォームプロジェクトでのみ利用可能**です。Windows レガシでは利用できないので注意しましょう。

# 作ってみる

:::note info
[RPA Challenge](https://www.rpachallenge.com/?lang=ja) から challenge_ja.xlsx をダウンロードしてプロジェクトフォルダに配置してください。
:::

## ワークフローファイルの作成
UiPath Studio でプロジェクトを作成したら、デザインリボンの``新規``から``コード化されたワークフロー``を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/f4056875-e07b-0677-7445-5332156534b3.png)

名前を入力して作成すると、空の ``Execute`` メソッドが定義されたクラスが出てきました。プロジェクト名がネームスペース、ワークフロー名がクラス名となっているようですね。

また、``Execute`` メソッドには ``[Workflow]`` という属性が付与されておりこのクラスのエントリポイントとなっているようです。``[Workflow]`` 属性が付与されているメソッドであればメソッドの名前は ``Execute`` でなくても問題ないようです。

:::note info
とはいえ、まぎらわしいのでエントリポイントのメソッド名は ``Execute`` としておくのが良いかと思います。
:::

今回はあまり難しいことは考えずに、``Execute`` メソッドに処理を追加していきます。

```csharp:RPAChallenge.cs
using CodedWorkflow_RPAChallenge.ObjectRepository;
using System;
using System.Collections.Generic;
using System.Data;
using UiPath.CodedWorkflows;
using UiPath.Core;
using UiPath.Core.Activities.Storage;
using UiPath.Orchestrator.Client.Models;
using UiPath.Testing;
using UiPath.Testing.Activities.TestData;
using UiPath.Testing.Activities.TestDataQueues.Enums;
using UiPath.Testing.Enums;
using UiPath.UIAutomationNext.API.Contracts;
using UiPath.UIAutomationNext.API.Models;
using UiPath.UIAutomationNext.Enums;

namespace CodedWorkflow_RPAChallenge
{
    public class RPAChallenge : CodedWorkflow
    {
        [Workflow]
        public void Execute()
        {
            // To start using services, use IntelliSense (CTRL + Space) to discover the available services:
            // e.g. system.GetAsset(...)

            // For accessing UI Elements from Object Repository, you can use the Descriptors class e.g:
            // var screen = uiAutomation.Open(Descriptors.MyApp.FirstScreen);
            // screen.Click(Descriptors.MyApp.FirstScreen.SettingsButton);
        }
    }
}
```

## オブジェクトリポジトリ
コード化されたオートメーションでの UI 要素の指定はオブジェクトリポジトリを使うことになります。

:::note info
セレクターを直接指定する方法もサポートされていますが、コードと UI 要素の分離ができるオブジェクトリポジトリを使うことが推奨になります。
```
var screen = uiAutomation.Open(Target.FromSelector("<html app = 'chrome.exe'/>"));
```
:::

オブジェクトリポジトリについて非常に分かりやすい記事が投稿されているのでそちらを確認ください。

https://www.cresco.co.jp/blog/entry/14668.html

https://qiita.com/miya_gis/items/855c99aa3ccd6663c951

https://qiita.com/Jun96427231/items/3ba6dd6c056368319eda

早速オブジェクトリポジトリに UI 要素を追加していきたいところですが、追加方法は省略します。上記記事を参考に皆さんでチャレンジしてください。

こちらが出来上がりのイメージです。3分クッキングみたいですみません。。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/ab55c7ee-6e34-c65b-f969-b92baab9e64b.png)

## 画面を開く

では、早速 RPA Challenge の画面を開く処理を追加しましょう。``uiAutomation`` の ``Open`` メソッドにオブジェクトリポジトリに登録されている画面名を渡すことで画面を開くことができます。

画面名の指定は、``Descriptors`` の定数を使う方法と文字列での指定の2通りがあります。文字列のハードコーディングを避ける意味で ``Descriptors`` の定数を利用したほうが良いかもです。

```csharp
// できればこちら
UiTargetApp screen = uiAutomation.Open(Descriptors.Chrome__Rpa_Challenge_app.Chrome__Rpa_Challenge);
// この方法でもOK
UiTargetApp screen = uiAutomation.Open("Chrome: Rpa Challenge");
```

作成されたワークフローの基底クラスである ``CodedWorkflow`` クラスには下記メンバ変数が存在しており主要アクティビティパッケージのアクティビティをメソッドとして公開しています。

**system** : UiPath.System.Activities に相当
**uiautomation** : UiPath.UIAutomation.Activities に相当
**testing** : UiPath.Testing.Activities に相当

``uiautomation.Open`` の戻り値として ``UiTargetApp`` 型の変数（今回は ``screen``）が返ってきます。この ``UiTargetApp`` 型の変数を使ってクリックや文字の入力等の UI 操作をしていくことになります。

続いて開始ボタンをクリックする処理を追加します。UI 要素のクリックは ``screen`` 変数の ``Click`` メソッドにて行います。メソッドの引数にオブジェクトリポジトリに登録している開始ボタンの UI 要素を指定してください。

```csharp
screen.Click(Descriptors.Chrome__Rpa_Challenge_app.Chrome__Rpa_Challenge.開始);
```

## Excel ファイルの処理
2023年12月6日現在のコード化されたオートメーションでは UiPath.Excel.Activities に該当する C# のクラスの提供がありません。そのため、何らかの方法で Excel ファイルからデータを取得する必要があります。

今回は NPOI を利用して実装を進めていこうかと思います。

https://github.com/nissl-lab/npoi

NPOI を使うにあたり、ワークフローに以下のネームスペースを追加します。

```csharp
using NPOI.SS.UserModel;
```

NPOI で Excel ファイルの操作が出来るようになったので、ファイルを開いてシートを取得する処理を追加します。

```csharp
IWorkbook book = WorkbookFactory.Create(@".\challenge_ja.xlsx");
ISheet sheet = book.GetSheetAt(0);
```

ここまでのコードはこちらになります。

<details><summary>RPAChallenge.cs</summary>

```csharp:RPAChallenge.cs
using System;
using System.Collections.Generic;
using System.Data;
using System.Threading;
using System.Threading.Tasks;
using UiPath.CodedWorkflows;
using UiPath.Core;
using UiPath.Core.Activities.Storage;
using UiPath.Orchestrator.Client.Models;
using UiPath.Testing;
using UiPath.Testing.Activities.TestData;
using UiPath.Testing.Activities.TestDataQueues.Enums;
using UiPath.Testing.Enums;
using UiPath.UIAutomationNext.API.Contracts;
using UiPath.UIAutomationNext.API.Models;
using UiPath.UIAutomationNext.Enums;
using CodedWorkflow_RPAChallenge.ObjectRepository;
using NPOI.SS.UserModel;

namespace CodedWorkflow_RPAChallenge
{
    public class RPAChallenge : CodedWorkflow
    {
        [Workflow]
        public void Execute()
        {
            //UiTargetApp screen = uiAutomation.Open("Chrome: Rpa Challenge");
            UiTargetApp screen = uiAutomation.Open(Descriptors.Chrome__Rpa_Challenge_app.Chrome__Rpa_Challenge);

            screen.Click(Descriptors.Chrome__Rpa_Challenge_app.Chrome__Rpa_Challenge.開始);

            IWorkbook book = WorkbookFactory.Create(@".\challenge_ja.xlsx");
            ISheet sheet = book.GetSheetAt(0);
        }
    }
}
```
</details>

## データを入力していく
続いて、1件づつデータを入力していきましょう。Excelファイルからシートの取得までできているので1行ずつのループを入れていきます。

ループの条件に ``sheet.LastRowNum`` を使うことでシートの最終行まで繰り返すようにしています。ループ内では行が取れないまたはインデックス 0番のセル（苗字）に値がなければループを抜けるようにしています。

```csharp
for (int i = 1; i <= sheet.LastRowNum; i++) {
    IRow row = sheet.GetRow(i);
    if (row == null) {
        break;
    }
    if (row.GetCell(0) == null || string.IsNullOrEmpty(row.GetCell(0).ToString())) {
        break;
    }

    // ここに1件ずづの処理を記述していきます
}
```

文字の入力には ``screen`` 変数の ``TypeInto`` メソッドを使用します。``文字を入力`` アクティビティの英語名と同じなので直感的にイメージしやすいですね。

``TypeInto`` メソッドの第一引数に入力先の UI 要素、第二引数に入力したい文字を指定します。ループ内に以下のコードを追加してください。

Excelのセルの値は ``row.GetCell(インデックス番号)`` で取得しています。

```csharp
screen.TypeInto(Descriptors.Chrome__Rpa_Challenge_app.Chrome__Rpa_Challenge.苗字, row.GetCell(0).ToString());
screen.TypeInto(Descriptors.Chrome__Rpa_Challenge_app.Chrome__Rpa_Challenge.名前, row.GetCell(1).ToString());
screen.TypeInto(Descriptors.Chrome__Rpa_Challenge_app.Chrome__Rpa_Challenge.会社名, row.GetCell(2).ToString());
screen.TypeInto(Descriptors.Chrome__Rpa_Challenge_app.Chrome__Rpa_Challenge.部署, row.GetCell(3).ToString());
screen.TypeInto(Descriptors.Chrome__Rpa_Challenge_app.Chrome__Rpa_Challenge.住所, row.GetCell(4).ToString());
screen.TypeInto(Descriptors.Chrome__Rpa_Challenge_app.Chrome__Rpa_Challenge.メールアドレス, row.GetCell(5).ToString());
screen.TypeInto(Descriptors.Chrome__Rpa_Challenge_app.Chrome__Rpa_Challenge.電話番号, row.GetCell(6).ToString());
```

最後に登録ボタンのクリック処理を追加して完成です。

```csharp
screen.Click(Descriptors.Chrome__Rpa_Challenge_app.Chrome__Rpa_Challenge.登録);
```

# 実行してみる
通常のワークフローと同様でデザインリボンから実行することができます。``ファイルを実行`` を選択して実行してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/3af7c5ad-cd98-7deb-421b-3a6ba3271983.png)

無事完走できましたが、イメージしていたより遅く感じませんでしたか？遅く感じた人は鋭いです。実は入力モードが ``ハードウェアイベント`` だったため処理がもっさりしていました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/2fd54ed9-0d10-97d8-800b-97c49d5e76d5.png)

## オプションを変えてみる
入力モードを ``ハードウェアイベント`` から ``シミュレート`` に変更してみましょう。クリックや文字を入力ごとに細かく変更することも出来ますが今回は一括での変更をしてみます。

全体に影響を与える変更は ``TargetAppOptions`` クラスを使います。 RPAChallenge クラスのメンバ変数として以下を追加します。

:::note info
``TargetAppOptions`` でプロパティを変更するときは、``OpenMode`` も必ず指定してください。
:::

```csharp
public TargetAppOptions targetAppOptions => new TargetAppOptions(){
    InteractionMode = NInteractionMode.Simulate,
    OpenMode = NAppOpenMode.IfNotOpen
};
```

メンバ変数 ``targetAppOptions`` を ``uiautomation.Open`` メソッドの第二引数として渡します。

```csharp
UiTargetApp screen = uiAutomation.Open(Descriptors.Chrome__Rpa_Challenge_app.Chrome__Rpa_Challenge, targetAppOptions);
```

この状態でもう一度実行してみましょう。処理も安定して実行も倍近く早くなったことが確認できるかと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/6233debc-fb39-427c-c13a-3b6ef5586479.png)

コードは最終的にこのようになっているはずです。
```csharp:RPAChallenge.cs
using System;
using System.Collections.Generic;
using System.Data;
using System.Threading;
using System.Threading.Tasks;
using UiPath.CodedWorkflows;
using UiPath.Core;
using UiPath.Core.Activities.Storage;
using UiPath.Orchestrator.Client.Models;
using UiPath.Testing;
using UiPath.Testing.Activities.TestData;
using UiPath.Testing.Activities.TestDataQueues.Enums;
using UiPath.Testing.Enums;
using UiPath.UIAutomationNext.API.Contracts;
using UiPath.UIAutomationNext.API.Models;
using UiPath.UIAutomationNext.Enums;
using CodedWorkflow_RPAChallenge.ObjectRepository;
using NPOI.SS.UserModel;

namespace CodedWorkflow_RPAChallenge
{
    public class RPAChallenge : CodedWorkflow
    {
        public TargetAppOptions targetAppOptions => new TargetAppOptions(){
            InteractionMode = NInteractionMode.Simulate,
            OpenMode = NAppOpenMode.IfNotOpen
        };

        [Workflow]
        public void Execute()
        {
            //UiTargetApp screen = uiAutomation.Open("Chrome: Rpa Challenge");
            UiTargetApp screen = uiAutomation.Open(Descriptors.Chrome__Rpa_Challenge_app.Chrome__Rpa_Challenge, targetAppOptions);

            IWorkbook book = WorkbookFactory.Create(@".\challenge_ja.xlsx");
            ISheet sheet = book.GetSheetAt(0);

            screen.Click(Descriptors.Chrome__Rpa_Challenge_app.Chrome__Rpa_Challenge.開始);

            for (int i = 1; i <= sheet.LastRowNum; i++) {
                IRow row = sheet.GetRow(i);
                if (row == null) {
                    break;
                }
                if (row.GetCell(0) == null || string.IsNullOrEmpty(row.GetCell(0).ToString())) {
                    break;
                }

                screen.TypeInto(Descriptors.Chrome__Rpa_Challenge_app.Chrome__Rpa_Challenge.苗字, row.GetCell(0).ToString());
                screen.TypeInto(Descriptors.Chrome__Rpa_Challenge_app.Chrome__Rpa_Challenge.名前, row.GetCell(1).ToString());
                screen.TypeInto(Descriptors.Chrome__Rpa_Challenge_app.Chrome__Rpa_Challenge.会社名, row.GetCell(2).ToString());
                screen.TypeInto(Descriptors.Chrome__Rpa_Challenge_app.Chrome__Rpa_Challenge.部署, row.GetCell(3).ToString());
                screen.TypeInto(Descriptors.Chrome__Rpa_Challenge_app.Chrome__Rpa_Challenge.住所, row.GetCell(4).ToString());
                screen.TypeInto(Descriptors.Chrome__Rpa_Challenge_app.Chrome__Rpa_Challenge.メールアドレス, row.GetCell(5).ToString());
                screen.TypeInto(Descriptors.Chrome__Rpa_Challenge_app.Chrome__Rpa_Challenge.電話番号, row.GetCell(6).ToString());
                
                screen.Click(Descriptors.Chrome__Rpa_Challenge_app.Chrome__Rpa_Challenge.登録);
            }
        }
    }
}
```

# おわりに
颯爽と現れたコード化されたオートメーションですがいかがだったでしょうか？

今までのローコード開発とは逆のアプローチの機能がリリースされましたが、今までのアクティビティでの開発が主流であることは変わらないのでご安心ください。この機能はプログラミング経験のある開発者やチームが生産性を損なわずに自動化処理を実装していくのに役に立つものと理解しています（Selenium 等で画面の自動化テストを実装していた方が UiPath でテストケースを記述する際に効果を発揮する気がしています）。

個人的にはオブジェクトリポジトリを使うことで UI 要素とコードの分離ができていてすっきりした記述が可能となっており今後の成長が楽しみな機能の一つになりました。

是非皆さんもお試しください！