@aryacode/dsh-engineering 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +9 -8
  3. package/cordis.patch.yml +1 -1
  4. package/entries/netlogic.js +266 -0
  5. package/entries/optix.js +13 -0
  6. package/package.json +10 -10
  7. package/vendor/l5x/SKILL.md +27 -7
  8. package/vendor/l5x/lib/engine/change-set.d.ts +9 -1
  9. package/vendor/l5x/lib/engine/change-set.js +32 -7
  10. package/vendor/l5x/lib/engine/documentation.d.ts +58 -0
  11. package/vendor/l5x/lib/engine/documentation.js +237 -0
  12. package/vendor/l5x/lib/engine/instructions.d.ts +50 -0
  13. package/vendor/l5x/lib/engine/instructions.js +107 -0
  14. package/vendor/l5x/lib/engine/ladder.d.ts +74 -0
  15. package/vendor/l5x/lib/engine/ladder.js +209 -0
  16. package/vendor/l5x/lib/index.d.ts +1 -0
  17. package/vendor/l5x/lib/index.js +3 -1
  18. package/vendor/l5x/lib/model/routines.js +6 -1
  19. package/vendor/l5x/lib/session.d.ts +20 -0
  20. package/vendor/l5x/lib/session.js +36 -0
  21. package/vendor/l5x/lib/tools/document.d.ts +18 -0
  22. package/vendor/l5x/lib/tools/document.js +119 -0
  23. package/vendor/l5x/lib/tools/drafts.js +109 -3
  24. package/vendor/optix/SKILL.md +68 -0
  25. package/vendor/optix/netlogic/SOURCE.json +12 -0
  26. package/vendor/optix/netlogic/index.json +1987 -0
  27. package/vendor/optix/netlogic/pages/accessing-project-nodes.md +113 -0
  28. package/vendor/optix/netlogic/pages/advanced-svg.md +124 -0
  29. package/vendor/optix/netlogic/pages/alarming.md +489 -0
  30. package/vendor/optix/netlogic/pages/async-tasks.md +236 -0
  31. package/vendor/optix/netlogic/pages/audit-signature.md +91 -0
  32. package/vendor/optix/netlogic/pages/colors.md +122 -0
  33. package/vendor/optix/netlogic/pages/companion-specs.md +77 -0
  34. package/vendor/optix/netlogic/pages/creating-objects.md +370 -0
  35. package/vendor/optix/netlogic/pages/database-interaction.md +437 -0
  36. package/vendor/optix/netlogic/pages/datagrids.md +123 -0
  37. package/vendor/optix/netlogic/pages/debugging-tips.md +605 -0
  38. package/vendor/optix/netlogic/pages/designtime-tags-import.md +531 -0
  39. package/vendor/optix/netlogic/pages/dialog-boxes.md +150 -0
  40. package/vendor/optix/netlogic/pages/dispatch-events.md +200 -0
  41. package/vendor/optix/netlogic/pages/dynamic-links.md +825 -0
  42. package/vendor/optix/netlogic/pages/element-access.md +1208 -0
  43. package/vendor/optix/netlogic/pages/engineering-units.md +298 -0
  44. package/vendor/optix/netlogic/pages/events.md +340 -0
  45. package/vendor/optix/netlogic/pages/execute-command.md +202 -0
  46. package/vendor/optix/netlogic/pages/fto-studio-cli.md +99 -0
  47. package/vendor/optix/netlogic/pages/good-practices.md +50 -0
  48. package/vendor/optix/netlogic/pages/information-model.md +89 -0
  49. package/vendor/optix/netlogic/pages/log-output.md +22 -0
  50. package/vendor/optix/netlogic/pages/managing-aliases.md +22 -0
  51. package/vendor/optix/netlogic/pages/manipulate-nodes.md +72 -0
  52. package/vendor/optix/netlogic/pages/mqtt.md +240 -0
  53. package/vendor/optix/netlogic/pages/netlogic-lifecycle.md +270 -0
  54. package/vendor/optix/netlogic/pages/netlogic-overview.md +155 -0
  55. package/vendor/optix/netlogic/pages/opcua.md +325 -0
  56. package/vendor/optix/netlogic/pages/plc-tags.md +219 -0
  57. package/vendor/optix/netlogic/pages/queries.md +1297 -0
  58. package/vendor/optix/netlogic/pages/random-generation.md +90 -0
  59. package/vendor/optix/netlogic/pages/recipes.md +375 -0
  60. package/vendor/optix/netlogic/pages/recipex.md +2383 -0
  61. package/vendor/optix/netlogic/pages/regex.md +15 -0
  62. package/vendor/optix/netlogic/pages/register-observers.md +346 -0
  63. package/vendor/optix/netlogic/pages/reports.md +141 -0
  64. package/vendor/optix/netlogic/pages/resource-uri.md +308 -0
  65. package/vendor/optix/netlogic/pages/runtime-tags-import.md +280 -0
  66. package/vendor/optix/netlogic/pages/serial-port.md +357 -0
  67. package/vendor/optix/netlogic/pages/sessions.md +58 -0
  68. package/vendor/optix/netlogic/pages/system-node.md +153 -0
  69. package/vendor/optix/netlogic/pages/template-library.md +127 -0
  70. package/vendor/optix/netlogic/pages/threading.md +236 -0
  71. package/vendor/optix/netlogic/pages/translations.md +417 -0
  72. package/vendor/optix/netlogic/pages/trends.md +128 -0
  73. package/vendor/optix/netlogic/pages/ui-animations.md +115 -0
  74. package/vendor/optix/netlogic/pages/users-groups.md +416 -0
  75. package/vendor/optix/netlogic/pages/variables-formatting.md +16 -0
  76. package/vendor/optix/netlogic/pages/variables-interaction.md +557 -0
  77. package/vendor/optix/netlogic/pages/variables-structure.md +155 -0
@@ -0,0 +1,2383 @@
1
+ # RecipeX
2
+
3
+ > [!NOTE]
4
+ > This feature is available starting from FactoryTalk Optix 1.7.X and should not be confused with [legacy recipe](./recipes.md) features.
5
+
6
+ ## Overview
7
+
8
+ The **RecipeX** module provides a new way to handle recipes in FactoryTalk Optix applications. Recipes are structured data templates used to store, retrieve, and transfer configuration parameters and data items. The RecipeX system supports:
9
+
10
+ - Recipe creation, duplication, renaming, and deletion
11
+ - Data item management and value manipulation
12
+ - Metadata handling with custom fields
13
+ - Recipe transfers between store and target locations
14
+ - Edit model management for recipe editing workflows
15
+
16
+ Before reading this chapter, please check the [Recipe Schemas Documentation](https://www.rockwellautomation.com/en-us/docs/factorytalk-optix/1-7-0/contents-ditamap/creating-projects/recipes/recipex-schemas.html) for an introduction to recipe schemas and their configuration in the FactoryTalk Optix HMI project.
17
+
18
+ > [!NOTE]
19
+ > If a recipe schema is edited, the Runtime will require a Refactor to update the database table structure accordingly. It is advised to backup your project before making changes to recipe schemas.
20
+
21
+ ## Core Types
22
+
23
+ ### FTOptix.RecipeX.RecipeId
24
+
25
+ Uniquely identifies a recipe using name and version information.
26
+
27
+ ```csharp
28
+ [MapDataType(NamespaceUri = Module.Uri, Number = 10)]
29
+ public class RecipeId : TypedStruct
30
+ {
31
+ public string Name { get; set; } // Recipe name
32
+ public string Version { get; set; } // Recipe version
33
+ }
34
+ ```
35
+
36
+ **Usage Example:**
37
+ ```csharp
38
+ var recipeId = new FTOptix.RecipeX.RecipeId { Name = "Recipe1", Version = "1.0" };
39
+ ```
40
+
41
+ ### Recipe
42
+
43
+ Represents a complete recipe with metadata and timestamps.
44
+
45
+ ```csharp
46
+ [MapDataType(NamespaceUri = Module.Uri, Number = 14)]
47
+ public classRecipe : TypedStruct
48
+ {
49
+ public FTOptix.RecipeX.RecipeId FTOptix.RecipeX.RecipeId { get; set; } // Recipe identifier
50
+ public DateTime RecipeSchemaTimestamp { get; set; } // Schema timestamp
51
+ public DateTime CreatedAt { get; set; } // Creation timestamp
52
+ public DateTime ModifiedAt { get; set; } // Last modification timestamp
53
+ public FTOptix.RecipeX.RecipeMetadata[] MetadataValues { get; set; } // Custom metadata fields
54
+ }
55
+ ```
56
+
57
+ ### RecipeMetadata
58
+
59
+ Represents custom metadata associated with a recipe.
60
+
61
+ ```csharp
62
+ [MapDataType(NamespaceUri = Module.Uri, Number = 12)]
63
+ public classRecipeMetadata : TypedStruct
64
+ {
65
+ public string Name { get; set; } // Metadata field name
66
+ public LocalizedText DisplayName { get; set; } // Localized display name
67
+ public NodeId DataTypeId { get; set; } // OPC UA data type reference
68
+ public object Value { get; set; } // Metadata value
69
+ }
70
+ ```
71
+
72
+ ### RecipeDataItem
73
+
74
+ Describes a data item within a recipe, including path information for accessing nested properties.
75
+
76
+ ```csharp
77
+ [MapDataType(NamespaceUri = Module.Uri, Number = 171)]
78
+ public classRecipeDataItem : TypedStruct
79
+ {
80
+ public string[] ItemRelativeBrowsePath { get; set; } // Path to the recipe item, relative to the schema's target node
81
+ public string[] DataItemRelativeBrowsePath { get; set; } // Path to data item, relative to the item node reached by ItemRelativeBrowsePath
82
+ public FTOptix.Core.ElementAccessStruct ElementAccess { get; set; } // Array element access (index/range)
83
+ public NodeId DataTypeId { get; set; } // Data type identifier
84
+ }
85
+ ```
86
+
87
+ ### FTOptix.RecipeX.ErrorPolicy
88
+
89
+ Defines error handling behavior for recipe operations.
90
+
91
+ ```csharp
92
+ public enum FTOptix.RecipeX.ErrorPolicy : int
93
+ {
94
+ Strict = 0, // Stop on first error
95
+ BestEffort = 1 // Continue despite errors
96
+ }
97
+ ```
98
+
99
+ ### The Edit Model
100
+
101
+ An **Edit Model** is a temporary, session-exclusive working copy of a recipe used to support editing workflows. Key characteristics:
102
+
103
+ - **Session-exclusive**: only one session can hold an Edit Model at a time; concurrent access is not allowed. Each session can have a dedicated Edit Model instance if needed.
104
+ - **Non-destructive**: changes made inside the Edit Model are isolated from the persisted recipe until explicitly committed back to the store.
105
+ - **Queryable**: the Edit Model supports filtering and sorting, making it suitable as the data source for recipe-editing UIs (e.g. grids, lists).
106
+ - **Lifecycle**: the Edit Model is created by loading a recipe via `TransferFromStoreToEditModel` and is automatically destroyed when released or when `TransferFromEditModelToStore` completes. The `requesterNodeId` parameter identifies the session owner and is used to enforce the exclusivity constraint. An `EditModel` can also be created programmatically via `CreateEditModel` for use cases that require more direct control over its lifecycle and interactions or retrieved from an existing interactive session where a `ListView` is being used by the user to interact with a recipe.
107
+
108
+ #### FTOptix.RecipeX.EditModel Class
109
+
110
+ The `FTOptix.RecipeX.EditModel` class exposes the following members. Obtain an instance by resolving the `EditModelNodeId` returned by `CreateEditModel` and casting it:
111
+
112
+ ```csharp
113
+ var editModel = (FTOptix.RecipeX.EditModel)InformationModel.Get(createResult.EditModelNodeId);
114
+ ```
115
+
116
+ **Properties:**
117
+
118
+ | Member | Type | Description |
119
+ |---|---|---|
120
+ | `RecipeSchemaNodeId` | `NodeId` | NodeId of the `FTOptix.RecipeX.RecipeSchema` that owns this Edit Model |
121
+ | `RequesterNodeId` | `NodeId` | NodeId of the session owner; used to enforce exclusivity |
122
+ | `TargetNodeId` | `NodeId` | NodeId of the target node the Edit Model was created against |
123
+ | `FTOptix.RecipeX.RecipeId` | `FTOptix.RecipeX.RecipeId` | Identifies which recipe is loaded in the Edit Model |
124
+ | `PendingChanges` | `bool` | `true` if the Edit Model contains unsaved changes relative to the store; set to `false` after a successful commit |
125
+
126
+ **Methods:**
127
+
128
+ | Method | Description |
129
+ |---|---|
130
+ | `TransferFromStore(FTOptix.RecipeX.RecipeId)` | Loads a recipe from the store into this Edit Model |
131
+ | `TransferToStore(FTOptix.RecipeX.RecipeId)` | Commits Edit Model changes back to the store |
132
+ | `TransferToTarget(NodeId targetNodeId, FTOptix.RecipeX.ErrorPolicy)` | Applies Edit Model values to a target node |
133
+ | `TransferFromTarget(NodeId targetNodeId, FTOptix.RecipeX.ErrorPolicy)` | Overwrites Edit Model values with the current state of a target node |
134
+ | `GetDataItemValue(NodeId itemNodeId, string[] dataItemRelativeBrowsePath, FTOptix.Core.ElementAccessStruct elementAccess, NodeId dataTypeId)` | Reads the current in-memory value of a data item held by this Edit Model, without transferring anything to/from the store or target |
135
+
136
+ > [!TIP]
137
+ > `GetDataItemValue` was added into FactoryTalk Optix 1.8.x and is the recommended way to inspect values that a user is currently editing (e.g. in a `ListView` bound to a recipe) **before** calling `TransferFromEditModelToStore` or `TransferFromEditModelToTarget`. Unlike `RecipeSchema.GetRecipeDataItemValue`, which reads from the persisted recipe in the store, `EditModel.GetDataItemValue` reads the live, unsaved value from the Edit Model itself, so it reflects any pending edits. Note that this method takes the resolved `itemNodeId` (the actual node reached by navigating `ItemRelativeBrowsePath` from the Edit Model's `TargetNodeId`), not the browse path itself.
138
+
139
+ Edit Model instances can be programmatically created and/or interacted with as shown in the examples below.
140
+
141
+ ### The Schema Editor (Design Time)
142
+
143
+ > [!NOTE]
144
+ > `RecipeSchema.Editor` is available starting from FactoryTalk Optix 1.8.x. It is **design-time only**: use it from a Design Time NetLogic to configure which items and data items belong to a recipe schema. It does not run at runtime.
145
+
146
+ `RecipeSchema.Editor` returns a `RecipeSchemaEditor` that builds the item structure of a recipe schema in code, instead of using the Studio schema editor UI. Browse paths passed to it are relative to the schema's `TargetNode`, exactly like `ItemRelativeBrowsePath`/`DataItemRelativeBrowsePath` described above.
147
+
148
+ ```csharp
149
+ var editor = recipeSchema.Editor;
150
+ ```
151
+
152
+ #### RecipeItemToAdd
153
+
154
+ Describes one item to add to the schema: either its full subtree, or a selected subset of its data items.
155
+
156
+ ```csharp
157
+ public struct RecipeItemToAdd
158
+ {
159
+ public string[] ItemBrowsePath { get; set; } // Path to the item, relative to TargetNode
160
+ public string[][] DataItemsBrowsePaths { get; set; } // Selected data items; null = full subtree
161
+ }
162
+ ```
163
+
164
+ - `new RecipeItemToAdd(itemBrowsePath)` — imports the item and its **entire** subtree of data items.
165
+ - `new RecipeItemToAdd(itemBrowsePath, dataItemsBrowsePaths)` — imports **only** the listed data items.
166
+
167
+ #### SchemaEditorResultCode
168
+
169
+ Result code returned by `RecipeSchemaEditor` methods.
170
+
171
+ ```csharp
172
+ public enum SchemaEditorResultCode
173
+ {
174
+ Success = 0,
175
+ ItemNotFoundInTree = 1,
176
+ UnsupportedNodeType = 2,
177
+ NotFoundInSchema = 4,
178
+ OperationFailed = 5
179
+ }
180
+ ```
181
+
182
+ #### RecipeSchemaEditor Methods Used Below
183
+
184
+ | Method | Description |
185
+ |---|---|
186
+ | `ClearSchema()` | Removes all items currently configured in the schema |
187
+ | `AddRecipeItems(RecipeItemToAdd[])` | Adds a batch of items (full subtree or selective data items) to the schema in a single call |
188
+
189
+ ## Understanding Data Items
190
+
191
+ ### What is a Data Item?
192
+
193
+ A **data item** is the fundamental building block of a recipe. It represents a single value that can be stored, retrieved, and transferred as part of a recipe. Data items map to variables or properties in your application model and provide a structured way to capture and apply configurations.
194
+
195
+ ### How Data Items Work
196
+
197
+ When you define a recipe schema in the FactoryTalk Optix designer, you select variables and properties to include in the recipe. Each selected variable becomes a **data item**. The recipe system then:
198
+
199
+ 1. **Captures** current values from the target location when saving
200
+ 2. **Stores** values in the database as part of the recipe
201
+ 3. **Restores** values to the target location when applying
202
+
203
+ ### Data Item Structure
204
+
205
+ Data items are hierarchically organized using **browse paths** - arrays of strings that represent the navigation path through your object model:
206
+
207
+ ```txt
208
+ ItemRelativeBrowsePath = ["Settings", "Motor"]
209
+ DataItemRelativeBrowsePath = ["Speed", "MaxRPM"]
210
+ ```
211
+
212
+ **Please note:** Both paths are **relative**, never absolute.
213
+ - `ItemRelativeBrowsePath` is relative to the **target node** configured on the `FTOptix.RecipeX.RecipeSchema` (e.g. `TargetNode = Section1`).
214
+ - `DataItemRelativeBrowsePath` is relative to the **item node** that is reached after navigating `ItemRelativeBrowsePath` from the target node.
215
+
216
+ This structure allows you to:
217
+ - Navigate through nested objects and folders
218
+ - Access properties within objects
219
+ - Handle array subscripts using `ElementAccess`
220
+
221
+ **Visual Example:**
222
+ ```txt
223
+ Target Object (Settings)
224
+ |- Motor (Item)
225
+ | |- Speed (DataItem) <- Gets/Sets here
226
+ | |- MaxRPM (DataItem) <- Gets/Sets here
227
+ |- Conveyor (Item)
228
+ |- Enable (DataItem)
229
+ ```
230
+
231
+ ### Common Data Item Scenarios
232
+
233
+ #### Simple Variable (No Nesting)
234
+
235
+ ```csharp
236
+ var itemPath = new string[] { "Temperature" };
237
+ var dataItemPath = new string[] { }; // Empty = direct variable
238
+ var elementAccess = new FTOptix.Core.ElementAccessStruct();
239
+
240
+ // Gets/sets "Temperature" variable directly
241
+ var result = schema.GetRecipeDataItemValue(
242
+ recipeId,
243
+ itemPath,
244
+ dataItemPath,
245
+ elementAccess);
246
+ ```
247
+
248
+ #### Nested Object Property
249
+
250
+ ```csharp
251
+ var itemPath = new string[] { "MotorSettings" };
252
+ var dataItemPath = new string[] { "Speed" };
253
+ var elementAccess = new FTOptix.Core.ElementAccessStruct();
254
+
255
+ // Gets/sets MotorSettings.Speed
256
+ var result = schema.GetRecipeDataItemValue(
257
+ recipeId,
258
+ itemPath,
259
+ dataItemPath,
260
+ elementAccess);
261
+ ```
262
+
263
+ #### Deeply Nested Property
264
+
265
+ ```csharp
266
+ var itemPath = new string[] { "Parameters", "Industrial", "Motor1" };
267
+ var dataItemPath = new string[] { "Configuration", "MaxSpeed" };
268
+ var elementAccess = new FTOptix.Core.ElementAccessStruct();
269
+
270
+ // Gets/sets Parameters -> Industrial -> Motor1 -> Configuration -> MaxSpeed
271
+ var result = schema.GetRecipeDataItemValue(
272
+ recipeId,
273
+ itemPath,
274
+ dataItemPath,
275
+ elementAccess);
276
+ ```
277
+
278
+ #### Array Element Access
279
+
280
+ ```csharp
281
+ var itemPath = new string[] { "Sensors" };
282
+ var dataItemPath = new string[] { "Readings" };
283
+ var elementAccess = new FTOptix.Core.ElementAccessStruct
284
+ {
285
+ Index = 0 // Get first array element
286
+ };
287
+
288
+ // Gets/sets Sensors.Readings[0]
289
+ var result = schema.GetRecipeDataItemValue(
290
+ recipeId,
291
+ itemPath,
292
+ dataItemPath,
293
+ elementAccess);
294
+ ```
295
+
296
+ ### Working with Data Items in Code
297
+
298
+ #### Retrieving All Data Items in a Recipe
299
+
300
+ ```csharp
301
+ [ExportMethod]
302
+ public void InspectDataItems(string recipeName, string version)
303
+ {
304
+ var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
305
+ var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = version };
306
+
307
+ // Get the structure of data items in this recipe
308
+ var result = schema.GetDataItems(recipeId);
309
+
310
+ if (result.ResultCode == GetDataItemsResultCode.Success)
311
+ {
312
+ Log.Info($"Recipe contains {result.DataItems.Length} data items:");
313
+
314
+ foreach (var dataItem in result.DataItems)
315
+ {
316
+ // Build readable path
317
+ var itemPath = string.Join("/", dataItem.ItemRelativeBrowsePath);
318
+ var dataPath = string.Join("/", dataItem.DataItemRelativeBrowsePath);
319
+
320
+ if (!string.IsNullOrEmpty(dataPath))
321
+ {
322
+ Log.Info($" {itemPath} -> {dataPath}");
323
+ }
324
+ else
325
+ {
326
+ Log.Info($" {itemPath}");
327
+ }
328
+ }
329
+ }
330
+ }
331
+ ```
332
+
333
+ #### Reading Individual Data Item Values
334
+
335
+ ```csharp
336
+ [ExportMethod]
337
+ public object ReadDataItemValue(string recipeName, string[] itemPath,
338
+ string[] dataItemPath)
339
+ {
340
+ var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
341
+ var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = "1.0" };
342
+
343
+ var result = schema.GetRecipeDataItemValue(
344
+ recipeId,
345
+ itemPath,
346
+ dataItemPath,
347
+ new FTOptix.Core.ElementAccessStruct());
348
+
349
+ if (result.ResultCode == GetRecipeDataItemValueResultCode.Success)
350
+ {
351
+ return result.DataItemValue;
352
+ }
353
+ else
354
+ {
355
+ Log.Error($"Failed to read data item: {result.ResultCode}");
356
+ return null;
357
+ }
358
+ }
359
+ ```
360
+
361
+ #### Writing Data Item Values
362
+
363
+ ```csharp
364
+ [ExportMethod]
365
+ public void WriteDataItemValue(string recipeName, string[] itemPath,
366
+ string[] dataItemPath, object value)
367
+ {
368
+ var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
369
+ var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = "1.0" };
370
+
371
+ var result = schema.SetRecipeDataItemValue(
372
+ recipeId,
373
+ itemPath,
374
+ dataItemPath,
375
+ new FTOptix.Core.ElementAccessStruct(),
376
+ value);
377
+
378
+ if (result == FTOptix.RecipeX.SetRecipeDataItemValueResultCode.Success)
379
+ {
380
+ Log.Info("Data item value updated");
381
+ }
382
+ else
383
+ {
384
+ Log.Error($"Failed to update data item: {result}");
385
+ }
386
+ }
387
+ ```
388
+
389
+ #### Comparing Current vs. Stored Values
390
+
391
+ ```csharp
392
+ [ExportMethod]
393
+ public bool CompareDataItemWithTarget(string recipeName, string[] itemPath,
394
+ string[] dataItemPath, string targetVariablePath)
395
+ {
396
+ var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
397
+ var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = "1.0" };
398
+ var target = LogicObject.Owner.GetVariable(targetVariablePath);
399
+
400
+ // Get stored recipe value
401
+ var recipeResult = schema.GetRecipeDataItemValue(
402
+ recipeId,
403
+ itemPath,
404
+ dataItemPath,
405
+ new FTOptix.Core.ElementAccessStruct());
406
+
407
+ if (recipeResult.ResultCode != GetRecipeDataItemValueResultCode.Success)
408
+ return false;
409
+
410
+ // Get current target value
411
+ var targetValue = target?.Value.Value;
412
+
413
+ // Compare
414
+ bool valuesMatch = Equals(recipeResult.DataItemValue, targetValue);
415
+
416
+ Log.Info($"Recipe value: {recipeResult.DataItemValue}");
417
+ Log.Info($"Target value: {targetValue}");
418
+ Log.Info($"Match: {valuesMatch}");
419
+
420
+ return valuesMatch;
421
+ }
422
+ ```
423
+
424
+ ### Data Item Tips and Best Practices
425
+
426
+ 1. **Path Ordering Matters**: The order of path segments must exactly match your object hierarchy
427
+ 2. **Empty Data Item Path**: If the item itself is the variable, use an empty array for `dataItemRelativeBrowsePath`
428
+ 3. **Type Consistency**: Ensure the value type matches the data item's data type
429
+ 4. **Array Indices**: Use `FTOptix.Core.ElementAccessStruct` only for array subscripts; regular object properties go in the data item path
430
+ 5. **Performance**: Reading/writing individual data items has overhead; consider batch operations when possible
431
+
432
+ ### Common Mistakes
433
+
434
+ **Wrong:** Using the whole path as item path
435
+ ```csharp
436
+ // Incorrect
437
+ var itemPath = new string[] { "Settings", "Motor", "Speed" };
438
+ var dataItemPath = new string[] { };
439
+ ```
440
+
441
+ **Right:** Splitting item and property paths
442
+ ```csharp
443
+ // Correct
444
+ var itemPath = new string[] { "Settings", "Motor" };
445
+ var dataItemPath = new string[] { "Speed" };
446
+ ```
447
+
448
+ ---
449
+
450
+ ## Result Types
451
+
452
+ All methods return result types containing both a result code and relevant data. These are used to handle success and error conditions consistently.
453
+
454
+ ### GetRecipesResult
455
+
456
+ Result of retrieving all recipes from a recipe schema.
457
+
458
+ ```csharp
459
+ [MapDataType(NamespaceUri = Module.Uri, Number = 182)]
460
+ public class GetRecipesResult : TypedStruct
461
+ {
462
+ public Recipe[] Recipes { get; set; } // Array of recipes
463
+ public GetRecipesResultCode ResultCode { get; set; } // Result code
464
+ }
465
+ ```
466
+
467
+ **Result Codes:**
468
+ - `Success = 0` - Recipes retrieved successfully
469
+ - `StoreError = 1` - Error accessing the store
470
+ - `GenericError = 2` - General error occurred
471
+
472
+ ### GetDataItemsResult
473
+
474
+ Result of retrieving data items from a recipe.
475
+
476
+ ```csharp
477
+ [MapDataType(NamespaceUri = Module.Uri, Number = 178)]
478
+ public class GetDataItemsResult : TypedStruct
479
+ {
480
+ public RecipeDataItem[] DataItems { get; set; } // Array of data items
481
+ public GetDataItemsResultCode ResultCode { get; set; } // Result code
482
+ }
483
+ ```
484
+
485
+ **Result Codes:**
486
+ - `Success = 0` - Data items retrieved successfully
487
+ - `RecipeNotFound = 1` - Specified recipe not found
488
+ - `StoreError = 2` - Error accessing the store
489
+ - `GenericError = 3` - General error occurred
490
+
491
+ ### GetRecipeDataItemValueResult
492
+
493
+ Result of retrieving a specific data item value from a recipe.
494
+
495
+ ```csharp
496
+ [MapDataType(NamespaceUri = Module.Uri, Number = 127)]
497
+ public class GetRecipeDataItemValueResult : TypedStruct
498
+ {
499
+ public object DataItemValue { get; set; } // Retrieved value (scalar or array)
500
+ public GetRecipeDataItemValueResultCode ResultCode { get; set; } // Result code
501
+ }
502
+ ```
503
+
504
+ **Result Codes:**
505
+ - `Success = 0` - Value retrieved successfully
506
+ - `GenericError = 1` - General error occurred
507
+ - `StoreError = 2` - Error accessing the store
508
+ - `DataItemNotFound = 3` - Data item not found in recipe
509
+ - `UnexpectedEmptyRecipeName = 4` - Recipe name is empty/null
510
+ - `RecipeNotFound = 5` - Recipe not found in store
511
+
512
+ ### GetDataItemValueResult
513
+
514
+ Result of `FTOptix.RecipeX.EditModel.GetDataItemValue`, used to read the current in-memory value of a data item held by an Edit Model (i.e. the value as currently edited, before it is transferred to the store or a target).
515
+
516
+ ```csharp
517
+ public class GetDataItemValueResult : TypedStruct
518
+ {
519
+ public object DataItemValue { get; set; } // Value currently held in the Edit Model (scalar or array)
520
+ public GetDataItemValueResultCode ResultCode { get; set; } // Result code
521
+ }
522
+ ```
523
+
524
+ **Result Codes:**
525
+ - `Success = 0` - Value retrieved successfully
526
+ - `GenericError = 1` - General error occurred
527
+ - `DataItemNotFound = 2` - Data item not found in the Edit Model
528
+
529
+ ### FTOptix.RecipeX.GetRecipeMetadataValueResult
530
+
531
+ Result of retrieving a single metadata value.
532
+
533
+ ```csharp
534
+ [MapDataType(NamespaceUri = Module.Uri, Number = 134)]
535
+ public classGetRecipeMetadataValueResult : TypedStruct
536
+ {
537
+ public RecipeMetadata MetadataValue { get; set; } // Retrieved metadata
538
+ public GetRecipeMetadataValueResultCode ResultCode { get; set; } // Result code
539
+ }
540
+ ```
541
+
542
+ **Result Codes:**
543
+ - `Success = 0` - Metadata retrieved successfully
544
+ - `GenericError = 1` - General error occurred
545
+ - `StoreError = 2` - Error accessing the store
546
+ - `UnexpectedEmptyRecipeName = 3` - Recipe name is empty/null
547
+ - `RecipeNotFound = 4` - Recipe not found in store
548
+ - `MetadataNotFound = 5` - Metadata field not found
549
+
550
+ ### GetRecipeMetadataValuesResult
551
+
552
+ Result of retrieving all metadata for a recipe.
553
+
554
+ ```csharp
555
+ [MapDataType(NamespaceUri = Module.Uri, Number = 138)]
556
+ public class GetRecipeMetadataValuesResult : TypedStruct
557
+ {
558
+ public RecipeMetadata[] MetadataValues { get; set; } // Array of metadata
559
+ public GetRecipeMetadataValuesResultCode ResultCode { get; set; } // Result code
560
+ }
561
+ ```
562
+
563
+ **Result Codes:**
564
+ - `Success = 0` - Metadata values retrieved successfully
565
+ - `GenericError = 1` - General error occurred
566
+ - `StoreError = 2` - Error accessing the store
567
+ - `UnexpectedEmptyRecipeName = 3` - Recipe name is empty/null
568
+ - `RecipeNotFound = 4` - Recipe not found in store
569
+
570
+ ### RenameRecipeResult
571
+
572
+ Result of renaming a recipe.
573
+
574
+ ```csharp
575
+ [MapDataType(NamespaceUri = Module.Uri, Number = 115)]
576
+ public class RenameRecipeResult : TypedStruct
577
+ {
578
+ public FTOptix.RecipeX.RecipeId NewRecipeId { get; set; } // New recipe identifier (after rename)
579
+ public RenameRecipeResultCode ResultCode { get; set; } // Result code
580
+ }
581
+ ```
582
+
583
+ **Result Codes:**
584
+ - `Success = 0` - Recipe renamed successfully
585
+ - `GenericError = 1` - General error occurred
586
+ - `StoreError = 2` - Error accessing the store
587
+ - `UnexpectedEmptyRecipeName = 3` - Recipe name is empty/null
588
+ - `SourceRecipeDoesNotExist = 4` - Recipe not found
589
+ - `TargetRecipeAlreadyExist = 5` - Target recipe name already in use
590
+
591
+ ### FTOptix.RecipeX.CreateEditModelResult
592
+
593
+ Result of creating an edit model.
594
+
595
+ ```csharp
596
+ [MapDataType(NamespaceUri = Module.Uri, Number = 142)]
597
+ public classCreateEditModelResult : TypedStruct
598
+ {
599
+ public NodeId EditModelNodeId { get; set; } // NodeId of created edit model
600
+ public CreateEditModelResultCode ResultCode { get; set; } // Result code
601
+ }
602
+ ```
603
+
604
+ **Result Codes:**
605
+ - `Success = 0` - Edit model created successfully
606
+ - `GenericError = 1` - General error occurred
607
+ - `StoreError = 2` - Error accessing the store
608
+ - `ParentNodeNotFound = 3` - Parent node for edit model not found
609
+ - `RequesterNodeNotFound = 4` - Requester node not found
610
+
611
+ ## FTOptix.RecipeX.RecipeSchema Class
612
+
613
+ The main class for recipe operations in FTOptix. Inherit from `UAObject` and provides methods for all recipe management tasks.
614
+
615
+ ```csharp
616
+ [MapType(NamespaceUri = Module.Uri, Number = 1)]
617
+ public classRecipeSchema : UAObject
618
+ {
619
+ // Properties
620
+ public NodeId TargetNode { get; set; } // Target node for recipe operations
621
+ public NodeId Store { get; set; } // Store node reference
622
+ public RecipeSchemaStatus Status { get; } // Current status
623
+ public UInt32 RemoteReadTimeout { get; set; } // Remote read timeout in ms
624
+ public PlaceholderChildNodeCollection<UAVariable> MetaData { get; } // Metadata variables
625
+
626
+ // Methods...
627
+ }
628
+ ```
629
+
630
+ ## FTOptix.RecipeX.RecipeSchema Methods
631
+
632
+ ### Recipe Management Methods
633
+
634
+ #### CreateRecipe
635
+
636
+ Creates a new empty recipe in the store.
637
+
638
+ ```csharp
639
+ public CreateRecipeResultCode CreateRecipe(FTOptix.RecipeX.RecipeId recipeId)
640
+ ```
641
+
642
+ **Parameters:**
643
+ - `recipeId` - Identifier with name and version
644
+
645
+ **Returns:** `CreateRecipeResultCode`
646
+ - `Success` - Recipe created successfully
647
+ - `GenericError` - General error occurred
648
+ - `StoreError` - Error accessing the store
649
+ - `UnexpectedEmptyRecipeName` - Recipe name is empty
650
+ - `RecipeAlreadyExist` - Recipe with same name/version exists
651
+ - `TargetNotFound` - Target node not found
652
+
653
+ **Example:**
654
+ ```csharp
655
+ var schema = (FTOptix.RecipeX.RecipeSchema)hmiProject.GetVariable("RecipeSchema1").Value;
656
+ var recipeId = new FTOptix.RecipeX.RecipeId { Name = "MyRecipe", Version = "1.0" };
657
+ var result = schema.CreateRecipe(recipeId);
658
+ if (result == CreateRecipeResultCode.Success)
659
+ {
660
+ Log.Info("Recipe created successfully");
661
+ }
662
+ ```
663
+
664
+ #### DeleteRecipe
665
+
666
+ Deletes a recipe from the store.
667
+
668
+ ```csharp
669
+ public DeleteRecipeResultCode DeleteRecipe(FTOptix.RecipeX.RecipeId recipeId)
670
+ ```
671
+
672
+ **Parameters:**
673
+ - `recipeId` - Recipe to delete
674
+
675
+ **Returns:** `DeleteRecipeResultCode`
676
+ - `Success` - Recipe deleted successfully
677
+ - `GenericError` - General error occurred
678
+ - `StoreError` - Error accessing the store
679
+ - `UnexpectedEmptyRecipeName` - Recipe name is empty
680
+ - `RecipeDoesNotExist` - Recipe not found
681
+
682
+ **Example:**
683
+ ```csharp
684
+ var result = schema.DeleteRecipe(recipeId);
685
+ ```
686
+
687
+ #### DuplicateRecipe
688
+
689
+ Creates a copy of an existing recipe with a new identifier.
690
+
691
+ ```csharp
692
+ public DuplicateRecipeResultCode DuplicateRecipe(FTOptix.RecipeX.RecipeId recipeId, FTOptix.RecipeX.RecipeId newRecipeId)
693
+ ```
694
+
695
+ **Parameters:**
696
+ - `recipeId` - Source recipe to duplicate
697
+ - `newRecipeId` - Identifier for the new recipe
698
+
699
+ **Returns:** `DuplicateRecipeResultCode`
700
+ - `Success` - Recipe duplicated successfully
701
+ - `GenericError` - General error during duplication
702
+ - `StoreError` - Error accessing the store
703
+ - `UnexpectedSourceEmptyRecipeName` - Source recipe name is empty
704
+ - `UnexpectedTargetEmptyRecipeName` - Target recipe name is empty
705
+ - `SourceRecipeDoesNotExist` - Source recipe not found
706
+ - `TargetRecipeAlreadyExist` - Target recipe name already exists
707
+ - `TargetNotFound` - Target node not found
708
+
709
+ **Example:**
710
+ ```csharp
711
+ var sourceId = new FTOptix.RecipeX.RecipeId { Name = "Recipe1", Version = "1.0" };
712
+ var targetId = new FTOptix.RecipeX.RecipeId { Name = "Recipe1_Copy", Version = "1.0" };
713
+ var result = schema.DuplicateRecipe(sourceId, targetId);
714
+ ```
715
+
716
+ #### RenameRecipe
717
+
718
+ Renames an existing recipe.
719
+
720
+ ```csharp
721
+ public RenameRecipeResult RenameRecipe(FTOptix.RecipeX.RecipeId recipeId, string newName)
722
+ ```
723
+
724
+ **Parameters:**
725
+ - `recipeId` - Recipe to rename
726
+ - `newName` - New recipe name
727
+
728
+ **Returns:** `RenameRecipeResult`
729
+ - `NewRecipeId` - Updated recipe identifier
730
+ - `ResultCode` - Operation result
731
+
732
+ **Example:**
733
+ ```csharp
734
+ var result = schema.RenameRecipe(recipeId, "NewRecipeName");
735
+ if (result.ResultCode == RenameRecipeResultCode.Success)
736
+ {
737
+ Log.Info($"Recipe renamed to: {result.NewRecipeId.Name}");
738
+ }
739
+ ```
740
+
741
+ ### Data Retrieval Methods
742
+
743
+ #### GetRecipes
744
+
745
+ Retrieves all recipes from the schema.
746
+
747
+ ```csharp
748
+ public GetRecipesResult GetRecipes()
749
+ ```
750
+
751
+ **Returns:** `GetRecipesResult`
752
+ - `Recipes` - Array of all recipes
753
+ - `ResultCode` - Operation result
754
+
755
+ **Example:**
756
+ ```csharp
757
+ var result = schema.GetRecipes();
758
+ if (result.ResultCode == GetRecipesResultCode.Success)
759
+ {
760
+ foreach (var recipe in result.Recipes)
761
+ {
762
+ Log.Info($"Recipe: {recipe.FTOptix.RecipeX.RecipeId.Name} v{recipe.FTOptix.RecipeX.RecipeId.Version}");
763
+ }
764
+ }
765
+ ```
766
+
767
+ #### GetDataItems
768
+
769
+ Retrieves the data item structure for a recipe.
770
+
771
+ ```csharp
772
+ public GetDataItemsResult GetDataItems(FTOptix.RecipeX.RecipeId recipeId)
773
+ ```
774
+
775
+ **Parameters:**
776
+ - `recipeId` - Recipe identifier
777
+
778
+ **Returns:** `GetDataItemsResult`
779
+ - `DataItems` - Array describing data items in the recipe
780
+ - `ResultCode` - Operation result
781
+
782
+ **Example:**
783
+ ```csharp
784
+ var result = schema.GetDataItems(recipeId);
785
+ if (result.ResultCode == GetDataItemsResultCode.Success)
786
+ {
787
+ foreach (var item in result.DataItems)
788
+ {
789
+ var itemPath = string.Join("/", item.ItemRelativeBrowsePath);
790
+ Log.Info($"Data Item: {itemPath}");
791
+ }
792
+ }
793
+ ```
794
+
795
+ #### GetRecipeDataItemValue
796
+
797
+ Retrieves a specific data item value from a recipe.
798
+
799
+ ```csharp
800
+ public GetRecipeDataItemValueResult GetRecipeDataItemValue(
801
+ FTOptix.RecipeX.RecipeId recipeId,
802
+ string[] itemRelativeBrowsePath,
803
+ string[] dataItemRelativeBrowsePath,
804
+ FTOptix.Core.ElementAccessStruct elementAccess)
805
+ ```
806
+
807
+ **Parameters:**
808
+ - `recipeId` - Recipe identifier
809
+ - `itemRelativeBrowsePath` - Browse path to item (array of path segments)
810
+ - `dataItemRelativeBrowsePath` - Browse path to data item within the item
811
+ - `elementAccess` - Array element access for subscripts (if applicable)
812
+
813
+ **Returns:** `GetRecipeDataItemValueResult`
814
+ - `DataItemValue` - Retrieved value (scalar or array)
815
+ - `ResultCode` - Operation result
816
+
817
+ **Example:**
818
+ ```csharp
819
+ var itemPath = new string[] { "Item1" };
820
+ var dataItemPath = new string[] { "Property1" };
821
+ var elementAccess = new FTOptix.Core.ElementAccessStruct(); // No array index
822
+
823
+ var result = schema.GetRecipeDataItemValue(recipeId, itemPath, dataItemPath, elementAccess);
824
+ if (result.ResultCode == GetRecipeDataItemValueResultCode.Success)
825
+ {
826
+ Log.Info($"Value: {result.DataItemValue}");
827
+ }
828
+ ```
829
+
830
+ #### GetRecipeMetadataValue
831
+
832
+ Retrieves a single metadata value.
833
+
834
+ ```csharp
835
+ public FTOptix.RecipeX.GetRecipeMetadataValueResult GetRecipeMetadataValue(FTOptix.RecipeX.RecipeId recipeId, string metadataName)
836
+ ```
837
+
838
+ **Parameters:**
839
+ - `recipeId` - Recipe identifier
840
+ - `metadataName` - Name of the metadata field
841
+
842
+ **Returns:** `FTOptix.RecipeX.GetRecipeMetadataValueResult`
843
+ - `MetadataValue` - Retrieved metadata
844
+ - `ResultCode` - Operation result
845
+
846
+ **Example:**
847
+ ```csharp
848
+ var result = schema.GetRecipeMetadataValue(recipeId, "Author");
849
+ if (result.ResultCode == GetRecipeMetadataValueResultCode.Success)
850
+ {
851
+ Log.Info($"Author: {result.MetadataValue.Value}");
852
+ }
853
+ ```
854
+
855
+ #### GetRecipeMetadataValues
856
+
857
+ Retrieves all metadata for a recipe.
858
+
859
+ ```csharp
860
+ public GetRecipeMetadataValuesResult GetRecipeMetadataValues(FTOptix.RecipeX.RecipeId recipeId)
861
+ ```
862
+
863
+ **Parameters:**
864
+ - `recipeId` - Recipe identifier
865
+
866
+ **Returns:** `GetRecipeMetadataValuesResult`
867
+ - `MetadataValues` - Array of all metadata fields
868
+ - `ResultCode` - Operation result
869
+
870
+ **Example:**
871
+ ```csharp
872
+ var result = schema.GetRecipeMetadataValues(recipeId);
873
+ if (result.ResultCode == GetRecipeMetadataValuesResultCode.Success)
874
+ {
875
+ foreach (var metadata in result.MetadataValues)
876
+ {
877
+ Log.Info($"{metadata.Name}: {metadata.Value}");
878
+ }
879
+ }
880
+ ```
881
+
882
+ ### Data Modification Methods
883
+
884
+ #### SetRecipeDataItemValue
885
+
886
+ Updates a data item value in a recipe.
887
+
888
+ ```csharp
889
+ public FTOptix.RecipeX.SetRecipeDataItemValueResultCode SetRecipeDataItemValue(
890
+ FTOptix.RecipeX.RecipeId recipeId,
891
+ string[] itemRelativeBrowsePath,
892
+ string[] dataItemRelativeBrowsePath,
893
+ FTOptix.Core.ElementAccessStruct elementAccess,
894
+ object value)
895
+ ```
896
+
897
+ **Parameters:**
898
+ - `recipeId` - Recipe identifier
899
+ - `itemRelativeBrowsePath` - Browse path to item
900
+ - `dataItemRelativeBrowsePath` - Browse path to data item
901
+ - `elementAccess` - Array element access
902
+ - `value` - New value to set
903
+
904
+ **Returns:** `FTOptix.RecipeX.SetRecipeDataItemValueResultCode`
905
+ - `Success` - Value set successfully
906
+ - `GenericError` - General error occurred
907
+ - `StoreError` - Error accessing the store
908
+ - `DataItemNotFound` - Data item not found
909
+ - `UnexpectedEmptyRecipeName` - Recipe name is empty
910
+ - `RecipeNotFound` - Recipe not found
911
+
912
+ **Example:**
913
+ ```csharp
914
+ var result = schema.SetRecipeDataItemValue(
915
+ recipeId,
916
+ new string[] { "Item1" },
917
+ new string[] { "Property1" },
918
+ new FTOptix.Core.ElementAccessStruct(),
919
+ 42);
920
+ ```
921
+
922
+ #### SetRecipeMetadataValue
923
+
924
+ Updates a metadata value.
925
+
926
+ ```csharp
927
+ public FTOptix.RecipeX.SetRecipeMetadataValueResultCode SetRecipeMetadataValue(
928
+ FTOptix.RecipeX.RecipeId recipeId,
929
+ string metadataName,
930
+ object value)
931
+ ```
932
+
933
+ **Parameters:**
934
+ - `recipeId` - Recipe identifier
935
+ - `metadataName` - Metadata field name
936
+ - `value` - New metadata value
937
+
938
+ **Returns:** `FTOptix.RecipeX.SetRecipeMetadataValueResultCode`
939
+ - `Success` - Metadata set successfully
940
+ - `GenericError` - General error occurred
941
+ - `StoreError` - Error accessing the store
942
+ - `UnexpectedEmptyRecipeName` - Recipe name is empty
943
+ - `RecipeNotFound` - Recipe not found
944
+ - `MetadataNotFound` - Metadata field not found
945
+
946
+ **Example:**
947
+ ```csharp
948
+ var result = schema.SetRecipeMetadataValue(recipeId, "Author", "John Doe");
949
+ ```
950
+
951
+ ### Transfer Methods
952
+
953
+ Transfer methods move recipes between three locations: **Store** (persistent database), **Edit Model** (temporary working copy), and **Target** (active runtime location).
954
+
955
+ #### TransferFromStoreToTarget
956
+
957
+ Loads a recipe from the store and applies it to a target location.
958
+
959
+ ```csharp
960
+ public FTOptix.RecipeX.TransferFromStoreToTargetResultCode TransferFromStoreToTarget(
961
+ FTOptix.RecipeX.RecipeId recipeId,
962
+ NodeId targetNodeId,
963
+ FTOptix.RecipeX.ErrorPolicy errorPolicy)
964
+ ```
965
+
966
+ **Parameters:**
967
+ - `recipeId` - Recipe to transfer
968
+ - `targetNodeId` - Destination node
969
+ - `errorPolicy` - Error handling mode
970
+
971
+ **Returns:** `FTOptix.RecipeX.TransferFromStoreToTargetResultCode`
972
+ - `Success` - Transfer successful
973
+ - `GenericError` - General error occurred
974
+ - `StoreError` - Error accessing the store
975
+ - `UnexpectedEmptyRecipeName` - Recipe name is empty
976
+ - `RecipeNotFound` - Recipe not found
977
+ - `TargetNotFound` - Target node not found
978
+ - `DataMismatch` - Data structure mismatch
979
+
980
+ **Example:**
981
+ ```csharp
982
+ var targetNode = hmiProject.GetVariable("TargetObject").Value;
983
+ var result = schema.TransferFromStoreToTarget(recipeId, targetNode, FTOptix.RecipeX.ErrorPolicy.Strict);
984
+ ```
985
+
986
+ #### TransferFromTargetToStore
987
+
988
+ Captures the current state of a target and saves it as a recipe.
989
+
990
+ ```csharp
991
+ public FTOptix.RecipeX.TransferFromTargetToStoreResultCode TransferFromTargetToStore(
992
+ FTOptix.RecipeX.RecipeId recipeId,
993
+ NodeId targetNodeId,
994
+ bool overwrite,
995
+ FTOptix.RecipeX.ErrorPolicy errorPolicy)
996
+ ```
997
+
998
+ **Parameters:**
999
+ - `recipeId` - Recipe identifier to create/update
1000
+ - `targetNodeId` - Source location to capture
1001
+ - `overwrite` - Whether to overwrite existing recipe
1002
+ - `errorPolicy` - Error handling mode
1003
+
1004
+ **Returns:** `FTOptix.RecipeX.TransferFromTargetToStoreResultCode`
1005
+ - `SuccessRecipeCreated` - New recipe created
1006
+ - `SuccessRecipeUpdated` - Existing recipe updated
1007
+ - `GenericError` - General error
1008
+ - `StoreError` - Store access error
1009
+ - `UnexpectedEmptyRecipeName` - Recipe name is empty
1010
+ - `RecipeAlreadyExists` - Recipe exists and overwrite is false
1011
+ - `TargetNotFound` - Target node not found
1012
+ - `DataMismatch` - Data structure mismatch
1013
+
1014
+ **Example:**
1015
+ ```csharp
1016
+ var result = schema.TransferFromTargetToStore(
1017
+ recipeId,
1018
+ targetNode,
1019
+ overwrite: true,
1020
+ FTOptix.RecipeX.ErrorPolicy.BestEffort);
1021
+ ```
1022
+
1023
+ #### TransferFromStoreToEditModel
1024
+
1025
+ Loads a recipe from the store into the session's Edit Model.
1026
+
1027
+ ```csharp
1028
+ public FTOptix.RecipeX.TransferFromStoreToEditModelResultCode TransferFromStoreToEditModel(
1029
+ NodeId requesterNodeId,
1030
+ FTOptix.RecipeX.RecipeId recipeId)
1031
+ ```
1032
+
1033
+ **Parameters:**
1034
+ - `requesterNodeId` - Node requesting the operation
1035
+ - `recipeId` - Recipe to load
1036
+
1037
+ **Returns:** `FTOptix.RecipeX.TransferFromStoreToEditModelResultCode`
1038
+ - `Success` - Transfer successful
1039
+ - `GenericError` - General error
1040
+ - `StoreError` - Store access error
1041
+ - `RequesterNodeNotFound` - Requester node not found
1042
+ - `UnexpectedEmptyRecipeName` - Recipe name is empty
1043
+ - `RecipeNotFound` - Recipe not found
1044
+ - `EditModelNodeNotFound` - Edit model not found
1045
+
1046
+ **Example:**
1047
+ ```csharp
1048
+ var requester = hmiProject.GetVariable("LogicObject").Value;
1049
+ var result = schema.TransferFromStoreToEditModel(requester, recipeId);
1050
+ ```
1051
+
1052
+ #### TransferFromEditModelToStore
1053
+
1054
+ Saves the edit model changes back to the store.
1055
+
1056
+ ```csharp
1057
+ public FTOptix.RecipeX.TransferFromEditModelToStoreResultCode TransferFromEditModelToStore(
1058
+ NodeId requesterNodeId,
1059
+ FTOptix.RecipeX.RecipeId recipeId)
1060
+ ```
1061
+
1062
+ **Parameters:**
1063
+ - `requesterNodeId` - Node requesting the operation
1064
+ - `recipeId` - Recipe identifier (for new recipes)
1065
+
1066
+ **Returns:** `FTOptix.RecipeX.TransferFromEditModelToStoreResultCode`
1067
+ - `SuccessRecipeCreated` - New recipe created
1068
+ - `SuccessRecipeUpdated` - Existing recipe updated
1069
+ - `GenericError` - General error
1070
+ - `StoreError` - Store access error
1071
+ - `RequesterNodeNotFound` - Requester node not found
1072
+ - `UnexpectedEmptyRecipeName` - Recipe name is empty
1073
+ - `TargetNotFound` - Target not found
1074
+ - `EditModelNodeNotFound` - Edit model not found
1075
+
1076
+ **Example:**
1077
+ ```csharp
1078
+ var result = schema.TransferFromEditModelToStore(requester, recipeId);
1079
+ ```
1080
+
1081
+ #### TransferFromEditModelToTarget
1082
+
1083
+ Applies edit model changes to a target location.
1084
+
1085
+ ```csharp
1086
+ public FTOptix.RecipeX.TransferFromEditModelToTargetResultCode TransferFromEditModelToTarget(
1087
+ NodeId requesterNodeId,
1088
+ NodeId targetNodeId,
1089
+ FTOptix.RecipeX.ErrorPolicy errorPolicy)
1090
+ ```
1091
+
1092
+ **Parameters:**
1093
+ - `requesterNodeId` - Node requesting the operation
1094
+ - `targetNodeId` - Destination node
1095
+ - `errorPolicy` - Error handling mode
1096
+
1097
+ **Returns:** `FTOptix.RecipeX.TransferFromEditModelToTargetResultCode`
1098
+ - `Success` - Transfer successful
1099
+ - `GenericError` - General error
1100
+ - `StoreError` - Store access error
1101
+ - `RequesterNodeNotFound` - Requester node not found
1102
+ - `TargetNotFound` - Target node not found
1103
+ - `EditModelNodeNotFound` - Edit model not found
1104
+ - `DataMismatch` - Data structure mismatch
1105
+
1106
+ #### TransferFromTargetToEditModel
1107
+
1108
+ Loads target state into edit model for editing.
1109
+
1110
+ ```csharp
1111
+ public FTOptix.RecipeX.TransferFromTargetToEditModelResultCode TransferFromTargetToEditModel(
1112
+ NodeId requesterNodeId,
1113
+ NodeId targetNodeId,
1114
+ FTOptix.RecipeX.ErrorPolicy errorPolicy)
1115
+ ```
1116
+
1117
+ **Parameters:**
1118
+ - `requesterNodeId` - Node requesting the operation
1119
+ - `targetNodeId` - Source location
1120
+ - `errorPolicy` - Error handling mode
1121
+
1122
+ **Returns:** `FTOptix.RecipeX.TransferFromTargetToEditModelResultCode`
1123
+ - `Success` - Transfer successful
1124
+ - `GenericError` - General error
1125
+ - `StoreError` - Store access error
1126
+ - `RequesterNodeNotFound` - Requester node not found
1127
+ - `TargetNotFound` - Target node not found
1128
+ - `EditModelNodeNotFound` - Edit model not found
1129
+ - `DataMismatch` - Data structure mismatch
1130
+
1131
+ ### Edit Model Methods
1132
+
1133
+ #### CreateEditModel
1134
+
1135
+ Creates a working copy of a recipe for editing.
1136
+
1137
+ ```csharp
1138
+ public FTOptix.RecipeX.CreateEditModelResult CreateEditModel(
1139
+ NodeId parentNodeId,
1140
+ NodeId requesterNodeId,
1141
+ NodeId targetNodeId,
1142
+ FTOptix.RecipeX.RecipeId recipeId)
1143
+ ```
1144
+
1145
+ **Parameters:**
1146
+ - `parentNodeId` - Parent node for the edit model
1147
+ - `requesterNodeId` - Node requesting the operation
1148
+ - `targetNodeId` - Target structure reference
1149
+ - `recipeId` - Recipe to edit
1150
+
1151
+ **Returns:** `FTOptix.RecipeX.CreateEditModelResult`
1152
+ - `EditModelNodeId` - NodeId of created edit model
1153
+ - `ResultCode` - Operation result
1154
+
1155
+ > [!TIP]
1156
+ > Once you have the `EditModelNodeId`, you can resolve it to the actual `FTOptix.RecipeX.EditModel` node and cast it to `FTOptix.RecipeX.EditModel`. The cast exposes additional members not available through `FTOptix.RecipeX.RecipeSchema`, including:
1157
+ > - `PendingChanges` — indicates whether the edit model has unsaved changes relative to the recipe in the store.
1158
+ > - Transfer methods directly on the `FTOptix.RecipeX.EditModel` instance (e.g. applying to target, committing to store) that operate implicitly on this edit model without having to pass the requester/node again.
1159
+
1160
+ > [!NOTE]
1161
+ > `CreateEditModel` behaves as **get-or-create**: if an edit model already exists for the given `parentNodeId`/`requesterNodeId` pair, it returns the existing instance instead of creating a new one. This is the correct way to access an edit model that was created and is managed by a `ListView` widget — pass the `ListView.NodeId` as both `parentNodeId` and `requesterNodeId`, and you will get back the live edit model the ListView is currently using.
1162
+
1163
+ **Example:**
1164
+ ```csharp
1165
+ var parent = hmiProject.GetVariable("Parent").Value;
1166
+ var requester = hmiProject.GetVariable("Requester").Value;
1167
+ var target = hmiProject.GetVariable("Target").Value;
1168
+
1169
+ var result = schema.CreateEditModel(parent, requester, target, recipeId);
1170
+ if (result.ResultCode == CreateEditModelResultCode.Success)
1171
+ {
1172
+ // Cast to FTOptix.RecipeX.EditModel to access PendingChanges and FTOptix.RecipeX.EditModel-level transfer methods
1173
+ var editModel = (FTOptix.RecipeX.EditModel)InformationModel.Get(result.EditModelNodeId);
1174
+ Log.Info($"Pending changes: {editModel.PendingChanges}");
1175
+ }
1176
+ ```
1177
+
1178
+ ### Maintenance Methods
1179
+
1180
+ #### RefactorRecipes
1181
+
1182
+ Asynchronously refactors the recipe schema. This is a long-running operation that completes asynchronously. When finished, a `RefactorCompleted` event is fired.
1183
+
1184
+ ```csharp
1185
+ public void RefactorRecipes()
1186
+ ```
1187
+
1188
+ **Notes:**
1189
+ - This is the only async method
1190
+ - Does not block the calling thread
1191
+ - Listen for `RefactorCompleted` event for completion notification
1192
+
1193
+ **Example:**
1194
+ ```csharp
1195
+ schema.RefactorRecipes();
1196
+ // Method returns immediately
1197
+ // Listen for RefactorCompleted event to know when complete
1198
+ ```
1199
+
1200
+ ## Events
1201
+
1202
+ ### RefactorCompleted
1203
+
1204
+ Fired when the `RefactorRecipes()` async method completes.
1205
+
1206
+ **Example:**
1207
+ ```csharp
1208
+ public class RecipeRefactorLogic : BaseNetLogic
1209
+ {
1210
+ public override void Start()
1211
+ {
1212
+ var schema = LogicObject.Owner.GetObject("RecipeSchema1");
1213
+ if (schema != null)
1214
+ {
1215
+ // Subscribe to event
1216
+ schema.EventOccurred += OnRefactorCompleted;
1217
+ }
1218
+ }
1219
+
1220
+ private void OnRefactorCompleted(IUANode sender, UAEventArgs args)
1221
+ {
1222
+ if (args.Event.BrowseName.Name == "RefactorCompleted")
1223
+ {
1224
+ Log.Info("Recipe refactoring completed");
1225
+ }
1226
+ }
1227
+ }
1228
+ ```
1229
+
1230
+ ### RecipeApplicationEvent
1231
+
1232
+ Fired when `TransferFromStoreToTarget` or `TransferFromEditModelToTarget` operations complete successfully.
1233
+
1234
+ ## Database Schema
1235
+
1236
+ RecipeX uses database tables to persist recipe data. Understanding this structure is useful for direct database queries.
1237
+
1238
+ ### Recipes Table
1239
+
1240
+ ```sql
1241
+ CREATE TABLE Recipes (
1242
+ Id, -- PRIMARY KEY [UInt32]
1243
+ Name, -- [String] Recipe name
1244
+ Version, -- [String] Recipe version
1245
+ RecipeSchemaName, -- [String] Schema name
1246
+ RecipeSchemaNamespaceUri, -- [String] Namespace URI
1247
+ RecipeSchemaTimestamp, -- [DateTime] Schema timestamp
1248
+ CreatedAt, -- [DateTime] Creation time
1249
+ ModifiedAt -- [DateTime] Last modification
1250
+ )
1251
+ ```
1252
+
1253
+ ### RecipeItems Table
1254
+
1255
+ ```sql
1256
+ CREATE TABLE RecipeItems (
1257
+ Id, -- PRIMARY KEY [UInt32]
1258
+ FTOptix.RecipeX.RecipeId, -- [UInt32] Foreign key to Recipes
1259
+ RelativeBrowsePath, -- [String] Item path
1260
+ TypeId -- [String] OPC UA type identifier
1261
+ )
1262
+ ```
1263
+
1264
+ ### RecipeDataItems Table
1265
+
1266
+ ```sql
1267
+ CREATE TABLE RecipeDataItems (
1268
+ Id, -- PRIMARY KEY [UInt32]
1269
+ RecipeItemId, -- [UInt32] Foreign key to RecipeItems
1270
+ RelativeBrowsePath, -- [String] Data item path
1271
+ ElementAccess, -- [String] Array element access
1272
+ DataTypeId, -- [String] OPC UA data type
1273
+ Value -- [String] Stored value
1274
+ )
1275
+ ```
1276
+
1277
+ ### RecipeMetadata Table
1278
+
1279
+ Dynamically created as `RecipeMetadata_<RecipeSchemaName>` per schema.
1280
+
1281
+ ```sql
1282
+ -- Example: RecipeMetadata_RecipeSchema1
1283
+ CREATE TABLE RecipeMetadata_RecipeSchema1 (
1284
+ FTOptix.RecipeX.RecipeId, -- Foreign key to Recipes
1285
+ Metadata1, -- [UserDefined] Custom metadata
1286
+ Metadata2, -- [UserDefined] Custom metadata
1287
+ -- ... additional metadata columns as defined in schema
1288
+ )
1289
+ ```
1290
+
1291
+ **Sample Query:**
1292
+ ```sql
1293
+ SELECT R.*, M.*
1294
+ FROM Recipes AS R
1295
+ LEFT JOIN "RecipeMetadata_RecipeSchema1" AS M
1296
+ ON R.Id = M.FTOptix.RecipeX.RecipeId
1297
+ WHERE R.RecipeSchemaName = 'RecipeSchema1'
1298
+ ORDER BY R.Name
1299
+ ```
1300
+
1301
+ ## Common Usage Patterns
1302
+
1303
+ ### Complete Recipe Workflow
1304
+
1305
+ ```csharp
1306
+ public class RecipeManager : BaseNetLogic
1307
+ {
1308
+ public override void Start()
1309
+ {
1310
+ var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
1311
+
1312
+ // Create recipe
1313
+ var recipeId = new FTOptix.RecipeX.RecipeId { Name = "Configuration1", Version = "1.0" };
1314
+ var createResult = schema.CreateRecipe(recipeId);
1315
+
1316
+ if (createResult == CreateRecipeResultCode.Success)
1317
+ {
1318
+ // Set metadata
1319
+ schema.SetRecipeMetadataValue(recipeId, "Author", "Engineering");
1320
+ schema.SetRecipeMetadataValue(recipeId, "Department", "Manufacturing");
1321
+
1322
+ // Set data values
1323
+ var itemPath = new string[] { "Settings" };
1324
+ var dataItemPath = new string[] { "Parameter1" };
1325
+ var elementAccess = new FTOptix.Core.ElementAccessStruct();
1326
+
1327
+ schema.SetRecipeDataItemValue(recipeId, itemPath, dataItemPath, elementAccess, 100);
1328
+
1329
+ Log.Info($"Recipe '{recipeId.Name}' created and configured");
1330
+ }
1331
+ }
1332
+ }
1333
+ ```
1334
+
1335
+ ### Loading and Applying a Recipe
1336
+
1337
+ ```csharp
1338
+ public class RecipeLoader : BaseNetLogic
1339
+ {
1340
+ [ExportMethod]
1341
+ public void ApplyRecipe(string recipeName, string version)
1342
+ {
1343
+ var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
1344
+ var targetNode = LogicObject.Owner.GetVariable("TargetObject").Value;
1345
+
1346
+ var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = version };
1347
+
1348
+ var result = schema.TransferFromStoreToTarget(
1349
+ recipeId,
1350
+ targetNode,
1351
+ FTOptix.RecipeX.ErrorPolicy.Strict);
1352
+
1353
+ if (result == FTOptix.RecipeX.TransferFromStoreToTargetResultCode.Success)
1354
+ {
1355
+ Log.Info($"Recipe '{recipeName}' applied to target");
1356
+ }
1357
+ else
1358
+ {
1359
+ Log.Error($"Failed to apply recipe: {result}");
1360
+ }
1361
+ }
1362
+ }
1363
+ ```
1364
+
1365
+ ### Capturing Configuration as Recipe
1366
+
1367
+ ```csharp
1368
+ public class ConfigurationCapture : BaseNetLogic
1369
+ {
1370
+ [ExportMethod]
1371
+ public void SaveCurrentConfiguration(string recipeName)
1372
+ {
1373
+ var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
1374
+ var targetNode = LogicObject.Owner.GetVariable("SourceObject").Value;
1375
+
1376
+ var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = "1.0" };
1377
+
1378
+ var result = schema.TransferFromTargetToStore(
1379
+ recipeId,
1380
+ targetNode,
1381
+ overwrite: true,
1382
+ FTOptix.RecipeX.ErrorPolicy.BestEffort);
1383
+
1384
+ if (result == FTOptix.RecipeX.TransferFromTargetToStoreResultCode.SuccessRecipeUpdated ||
1385
+ result == FTOptix.RecipeX.TransferFromTargetToStoreResultCode.SuccessRecipeCreated)
1386
+ {
1387
+ Log.Info($"Configuration saved as '{recipeName}'");
1388
+ }
1389
+ }
1390
+ }
1391
+ ```
1392
+
1393
+ ### Listing All Recipes
1394
+
1395
+ ```csharp
1396
+ public class RecipeList : BaseNetLogic
1397
+ {
1398
+ [ExportMethod]
1399
+ public void ListAllRecipes()
1400
+ {
1401
+ var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
1402
+
1403
+ var result = schema.GetRecipes();
1404
+
1405
+ if (result.ResultCode == GetRecipesResultCode.Success)
1406
+ {
1407
+ foreach (var recipe in result.Recipes)
1408
+ {
1409
+ var metadata = schema.GetRecipeMetadataValues(recipe.FTOptix.RecipeX.RecipeId);
1410
+
1411
+ Log.Info($"Recipe: {recipe.FTOptix.RecipeX.RecipeId.Name} v{recipe.FTOptix.RecipeX.RecipeId.Version}");
1412
+ Log.Info($" Created: {recipe.CreatedAt}");
1413
+ Log.Info($" Modified: {recipe.ModifiedAt}");
1414
+
1415
+ if (metadata.ResultCode == GetRecipeMetadataValuesResultCode.Success)
1416
+ {
1417
+ foreach (var meta in metadata.MetadataValues)
1418
+ {
1419
+ Log.Info($" {meta.Name}: {meta.Value}");
1420
+ }
1421
+ }
1422
+ }
1423
+ }
1424
+ }
1425
+ }
1426
+ ```
1427
+
1428
+ ## Practical Examples
1429
+
1430
+ ### Configure a Recipe Schema at Design Time
1431
+
1432
+ Use `RecipeSchema.Editor` from a Design Time NetLogic to (re)build a schema's item structure: import one item's full subtree, and import only selected data items from other items.
1433
+
1434
+ ```csharp
1435
+ public class RecipeSchemaLogic : BaseNetLogic
1436
+ {
1437
+ // Configures MyRecipeSchema at design time: MyMotor1 gets its full subtree, MyMotor2/MyMotor3 get only selected data items.
1438
+ [ExportMethod]
1439
+ public void ConfigureMotorRecipeSchema()
1440
+ {
1441
+ if (LogicObject.Owner is not RecipeSchema recipeSchema)
1442
+ {
1443
+ Log.Error("RecipeSchemaLogic", "Owner is not a RecipeSchema");
1444
+ return;
1445
+ }
1446
+
1447
+ var editor = recipeSchema.Editor;
1448
+
1449
+ try
1450
+ {
1451
+ var clearResult = editor.ClearSchema();
1452
+ if (clearResult != SchemaEditorResultCode.Success)
1453
+ {
1454
+ Log.Error("RecipeSchemaLogic", $"ClearSchema failed: {clearResult}");
1455
+ return;
1456
+ }
1457
+
1458
+ var itemsToAdd = new RecipeItemToAdd[]
1459
+ {
1460
+ // Full subtree: Enable, Speed, Acceleration
1461
+ new(new string[] { "MyMotor1" }),
1462
+ // Selective: Speed only
1463
+ new(new string[] { "MyMotor2" }, new string[][] { new string[] { "Speed" } }),
1464
+ // Selective: Acceleration only
1465
+ new(new string[] { "MyMotor3" }, new string[][] { new string[] { "Acceleration" } })
1466
+ };
1467
+
1468
+ var addResult = editor.AddRecipeItems(itemsToAdd);
1469
+ switch (addResult)
1470
+ {
1471
+ case SchemaEditorResultCode.Success:
1472
+ Log.Info("RecipeSchemaLogic", "Recipe schema configured successfully");
1473
+ break;
1474
+ case SchemaEditorResultCode.ItemNotFoundInTree:
1475
+ Log.Error("RecipeSchemaLogic", "AddRecipeItems failed: ItemNotFoundInTree (an item path was not found under TargetNode)");
1476
+ break;
1477
+ case SchemaEditorResultCode.UnsupportedNodeType:
1478
+ Log.Error("RecipeSchemaLogic", "AddRecipeItems failed: UnsupportedNodeType (an item resolved to an unsupported node type)");
1479
+ break;
1480
+ case SchemaEditorResultCode.NotFoundInSchema:
1481
+ Log.Error("RecipeSchemaLogic", "AddRecipeItems failed: NotFoundInSchema");
1482
+ break;
1483
+ case SchemaEditorResultCode.OperationFailed:
1484
+ Log.Error("RecipeSchemaLogic", "AddRecipeItems failed: OperationFailed");
1485
+ break;
1486
+ default:
1487
+ Log.Error("RecipeSchemaLogic", $"AddRecipeItems failed: {addResult}");
1488
+ break;
1489
+ }
1490
+ }
1491
+ catch (Exception ex)
1492
+ {
1493
+ Log.Error("RecipeSchemaLogic", $"Unexpected exception configuring recipe schema: {ex.Message}");
1494
+ }
1495
+ }
1496
+ }
1497
+ ```
1498
+
1499
+ Here, the schema's `TargetNode` points at a folder containing `MyMotor1`, `MyMotor2` and `MyMotor3` (each with `Enable`, `Speed` and `Acceleration` variables):
1500
+ - `MyMotor1` is added with no `DataItemsBrowsePaths` — its entire subtree (`Enable`, `Speed`, `Acceleration`) is imported.
1501
+ - `MyMotor2` and `MyMotor3` are added with an explicit `DataItemsBrowsePaths`, so only `Speed` (respectively `Acceleration`) is imported for them.
1502
+
1503
+ > [!NOTE]
1504
+ > Calling `ClearSchema()` before `AddRecipeItems(...)` resets the schema's item structure before rebuilding it, which is useful when re-running this logic (e.g. after changing which items/data items should be included).
1505
+
1506
+ ### Checking if a Recipe Exists
1507
+
1508
+ Verify whether a recipe exists before attempting operations:
1509
+
1510
+ ```csharp
1511
+ [ExportMethod]
1512
+ public bool RecipeExists(string recipeName, string version)
1513
+ {
1514
+ var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
1515
+ if (schema == null || string.IsNullOrEmpty(recipeName))
1516
+ return false;
1517
+
1518
+ var result = schema.GetRecipes();
1519
+ if (result.ResultCode != GetRecipesResultCode.Success)
1520
+ return false;
1521
+
1522
+ // Check if recipe matches
1523
+ foreach (var recipe in result.Recipes)
1524
+ {
1525
+ if (recipe.FTOptix.RecipeX.RecipeId.Name == recipeName &&
1526
+ recipe.FTOptix.RecipeX.RecipeId.Version == version)
1527
+ return true;
1528
+ }
1529
+
1530
+ return false;
1531
+ }
1532
+ ```
1533
+
1534
+ ### Safe Recipe Application with Error Handling
1535
+
1536
+ Apply a recipe with comprehensive error handling and logging:
1537
+
1538
+ ```csharp
1539
+ [ExportMethod]
1540
+ public void SafeApplyRecipe(string recipeName, string version)
1541
+ {
1542
+ var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
1543
+ if (schema == null)
1544
+ {
1545
+ Log.Error("Recipe schema not found");
1546
+ return;
1547
+ }
1548
+
1549
+ var target = LogicObject.Owner.GetVariable("TargetObject").Value;
1550
+ if (target == null)
1551
+ {
1552
+ Log.Error("Target object not found");
1553
+ return;
1554
+ }
1555
+
1556
+ var recipeId = new FTOptix.RecipeX.RecipeId
1557
+ {
1558
+ Name = recipeName,
1559
+ Version = version
1560
+ };
1561
+
1562
+ // Check if recipe exists first
1563
+ var recipesResult = schema.GetRecipes();
1564
+ bool recipeExists = false;
1565
+
1566
+ if (recipesResult.ResultCode == GetRecipesResultCode.Success)
1567
+ {
1568
+ recipeExists = recipesResult.Recipes.Any(r =>
1569
+ r.FTOptix.RecipeX.RecipeId.Name == recipeName &&
1570
+ r.FTOptix.RecipeX.RecipeId.Version == version);
1571
+ }
1572
+
1573
+ if (!recipeExists)
1574
+ {
1575
+ Log.Error($"Recipe '{recipeName}' v{version} not found");
1576
+ return;
1577
+ }
1578
+
1579
+ // Apply the recipe
1580
+ var result = schema.TransferFromStoreToTarget(
1581
+ recipeId,
1582
+ target,
1583
+ FTOptix.RecipeX.ErrorPolicy.Strict);
1584
+
1585
+ if (result == FTOptix.RecipeX.TransferFromStoreToTargetResultCode.Success)
1586
+ {
1587
+ Log.Info($"Recipe '{recipeName}' applied successfully");
1588
+ }
1589
+ else
1590
+ {
1591
+ switch (result)
1592
+ {
1593
+ case FTOptix.RecipeX.TransferFromStoreToTargetResultCode.RecipeNotFound:
1594
+ Log.Error("Recipe not found in store");
1595
+ break;
1596
+ case FTOptix.RecipeX.TransferFromStoreToTargetResultCode.TargetNotFound:
1597
+ Log.Error("Target node not found");
1598
+ break;
1599
+ case FTOptix.RecipeX.TransferFromStoreToTargetResultCode.DataMismatch:
1600
+ Log.Error("Recipe structure does not match target");
1601
+ break;
1602
+ case FTOptix.RecipeX.TransferFromStoreToTargetResultCode.StoreError:
1603
+ Log.Error("Error accessing recipe store");
1604
+ break;
1605
+ default:
1606
+ Log.Error($"Failed to apply recipe: {result}");
1607
+ break;
1608
+ }
1609
+ }
1610
+ }
1611
+ ```
1612
+
1613
+ ### Create Recipe from Current Target State
1614
+
1615
+ Capture current configuration as a recipe:
1616
+
1617
+ ```csharp
1618
+ [ExportMethod]
1619
+ public void CaptureAsRecipe(string recipeName)
1620
+ {
1621
+ var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
1622
+ var source = LogicObject.Owner.GetVariable("SourceObject").Value;
1623
+
1624
+ if (schema == null || source == null)
1625
+ {
1626
+ Log.Error("Recipe schema or source not found");
1627
+ return;
1628
+ }
1629
+
1630
+ var recipeId = new FTOptix.RecipeX.RecipeId
1631
+ {
1632
+ Name = recipeName,
1633
+ Version = DateTime.UtcNow.ToString("yyyyMMdd_HHmmss")
1634
+ };
1635
+
1636
+ // Capture and save
1637
+ var result = schema.TransferFromTargetToStore(
1638
+ recipeId,
1639
+ source,
1640
+ overwrite: false, // Don't overwrite existing
1641
+ FTOptix.RecipeX.ErrorPolicy.Strict);
1642
+
1643
+ switch (result)
1644
+ {
1645
+ case FTOptix.RecipeX.TransferFromTargetToStoreResultCode.SuccessRecipeCreated:
1646
+ Log.Info($"Recipe '{recipeName}' created and saved");
1647
+ // Optionally set metadata
1648
+ schema.SetRecipeMetadataValue(recipeId, "CaptureTime", DateTime.UtcNow);
1649
+ schema.SetRecipeMetadataValue(recipeId, "CapturedBy", "AutoCapture");
1650
+ break;
1651
+
1652
+ case FTOptix.RecipeX.TransferFromTargetToStoreResultCode.SuccessRecipeUpdated:
1653
+ Log.Info($"Recipe '{recipeName}' updated");
1654
+ break;
1655
+
1656
+ case FTOptix.RecipeX.TransferFromTargetToStoreResultCode.RecipeAlreadyExists:
1657
+ Log.Warning($"Recipe '{recipeName}' already exists");
1658
+ break;
1659
+
1660
+ case FTOptix.RecipeX.TransferFromTargetToStoreResultCode.DataMismatch:
1661
+ Log.Error("Source structure does not match recipe schema");
1662
+ break;
1663
+
1664
+ default:
1665
+ Log.Error($"Failed to capture recipe: {result}");
1666
+ break;
1667
+ }
1668
+ }
1669
+ ```
1670
+
1671
+ ### Batch Duplicate Recipe with Metadata
1672
+
1673
+ Duplicate a recipe while updating metadata:
1674
+
1675
+ ```csharp
1676
+ [ExportMethod]
1677
+ public void DuplicateRecipeWithMetadata(string sourceRecipe, string targetRecipe)
1678
+ {
1679
+ var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
1680
+ if (schema == null)
1681
+ return;
1682
+
1683
+ // Get source metadata
1684
+ var sourceId = new FTOptix.RecipeX.RecipeId { Name = sourceRecipe, Version = "1.0" };
1685
+ var metadataResult = schema.GetRecipeMetadataValues(sourceId);
1686
+
1687
+ if (metadataResult.ResultCode != GetRecipeMetadataValuesResultCode.Success)
1688
+ {
1689
+ Log.Error("Failed to retrieve source metadata");
1690
+ return;
1691
+ }
1692
+
1693
+ // Duplicate the recipe
1694
+ var targetId = new FTOptix.RecipeX.RecipeId { Name = targetRecipe, Version = "1.0" };
1695
+ var dupResult = schema.DuplicateRecipe(sourceId, targetId);
1696
+
1697
+ if (dupResult != DuplicateRecipeResultCode.Success)
1698
+ {
1699
+ Log.Error($"Failed to duplicate recipe: {dupResult}");
1700
+ return;
1701
+ }
1702
+
1703
+ // Copy metadata from source
1704
+ foreach (var metadata in metadataResult.MetadataValues)
1705
+ {
1706
+ schema.SetRecipeMetadataValue(targetId, metadata.Name, metadata.Value);
1707
+ }
1708
+
1709
+ // Update some fields
1710
+ schema.SetRecipeMetadataValue(targetId, "DuplicatedFrom", sourceRecipe);
1711
+ schema.SetRecipeMetadataValue(targetId, "DuplicatedAt", DateTime.UtcNow);
1712
+
1713
+ Log.Info($"Recipe '{targetRecipe}' created as copy of '{sourceRecipe}'");
1714
+ }
1715
+ ```
1716
+
1717
+ ### Interactive Recipe Selection and Application
1718
+
1719
+ Create an interactive experience for selecting and applying recipes:
1720
+
1721
+ ```csharp
1722
+ [ExportMethod]
1723
+ public string[] GetAvailableRecipes()
1724
+ {
1725
+ var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
1726
+ if (schema == null)
1727
+ return new string[] { };
1728
+
1729
+ var result = schema.GetRecipes();
1730
+ if (result.ResultCode != GetRecipesResultCode.Success)
1731
+ return new string[] { };
1732
+
1733
+ return result.Recipes
1734
+ .Select(r => $"{r.FTOptix.RecipeX.RecipeId.Name} (v{r.FTOptix.RecipeX.RecipeId.Version})")
1735
+ .ToArray();
1736
+ }
1737
+
1738
+ [ExportMethod]
1739
+ public void ApplySelectedRecipe(string recipeName, string version)
1740
+ {
1741
+ var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
1742
+ var target = LogicObject.Owner.GetVariable("TargetObject").Value;
1743
+
1744
+ if (schema == null || target == null)
1745
+ return;
1746
+
1747
+ var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = version };
1748
+ var result = schema.TransferFromStoreToTarget(recipeId, target, FTOptix.RecipeX.ErrorPolicy.Strict);
1749
+
1750
+ if (result == FTOptix.RecipeX.TransferFromStoreToTargetResultCode.Success)
1751
+ {
1752
+ Log.Info($"Applied recipe: {recipeName}");
1753
+ // Update UI display variable
1754
+ LogicObject.Owner.GetVariable("CurrentRecipe").Value = recipeName;
1755
+ }
1756
+ else
1757
+ {
1758
+ Log.Error($"Application failed: {result}");
1759
+ }
1760
+ }
1761
+
1762
+ [ExportMethod]
1763
+ public Dictionary<string, object> GetRecipeDetails(string recipeName, string version)
1764
+ {
1765
+ var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
1766
+ var details = new Dictionary<string, object>();
1767
+
1768
+ var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = version };
1769
+
1770
+ // Get general recipe info
1771
+ var recipesResult = schema.GetRecipes();
1772
+ if (recipesResult.ResultCode == GetRecipesResultCode.Success)
1773
+ {
1774
+ var recipe = recipesResult.Recipes.FirstOrDefault(r =>
1775
+ r.FTOptix.RecipeX.RecipeId.Name == recipeName &&
1776
+ r.FTOptix.RecipeX.RecipeId.Version == version);
1777
+
1778
+ if (recipe != null)
1779
+ {
1780
+ details["Name"] = recipe.FTOptix.RecipeX.RecipeId.Name;
1781
+ details["Version"] = recipe.FTOptix.RecipeX.RecipeId.Version;
1782
+ details["CreatedAt"] = recipe.CreatedAt;
1783
+ details["ModifiedAt"] = recipe.ModifiedAt;
1784
+ }
1785
+ }
1786
+
1787
+ // Get metadata
1788
+ var metaResult = schema.GetRecipeMetadataValues(recipeId);
1789
+ if (metaResult.ResultCode == GetRecipeMetadataValuesResultCode.Success)
1790
+ {
1791
+ var metadata = new Dictionary<string, object>();
1792
+ foreach (var meta in metaResult.MetadataValues)
1793
+ {
1794
+ metadata[meta.Name] = meta.Value;
1795
+ }
1796
+ details["Metadata"] = metadata;
1797
+ }
1798
+
1799
+ // Get data items count
1800
+ var itemsResult = schema.GetDataItems(recipeId);
1801
+ if (itemsResult.ResultCode == GetDataItemsResultCode.Success)
1802
+ {
1803
+ details["DataItemCount"] = itemsResult.DataItems.Length;
1804
+ }
1805
+
1806
+ return details;
1807
+ }
1808
+ ```
1809
+
1810
+ ### Recipe Validation and Consistency Check
1811
+
1812
+ Verify recipe consistency and values:
1813
+
1814
+ ```csharp
1815
+ [ExportMethod]
1816
+ public bool ValidateRecipe(string recipeName, string version)
1817
+ {
1818
+ var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
1819
+ if (schema == null)
1820
+ return false;
1821
+
1822
+ var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = version };
1823
+
1824
+ // Check recipe exists
1825
+ var recipesResult = schema.GetRecipes();
1826
+ if (recipesResult.ResultCode != GetRecipesResultCode.Success)
1827
+ {
1828
+ Log.Error("Failed to retrieve recipes");
1829
+ return false;
1830
+ }
1831
+
1832
+ var recipeExists = recipesResult.Recipes.Any(r =>
1833
+ r.FTOptix.RecipeX.RecipeId.Name == recipeName &&
1834
+ r.FTOptix.RecipeX.RecipeId.Version == version);
1835
+
1836
+ if (!recipeExists)
1837
+ {
1838
+ Log.Error($"Recipe '{recipeName}' not found");
1839
+ return false;
1840
+ }
1841
+
1842
+ // Validate data items
1843
+ var itemsResult = schema.GetDataItems(recipeId);
1844
+ if (itemsResult.ResultCode != GetDataItemsResultCode.Success)
1845
+ {
1846
+ Log.Error("Failed to retrieve data items");
1847
+ return false;
1848
+ }
1849
+
1850
+ if (itemsResult.DataItems.Length == 0)
1851
+ {
1852
+ Log.Warning("Recipe contains no data items");
1853
+ }
1854
+
1855
+ // Validate individual values
1856
+ bool allValid = true;
1857
+ foreach (var item in itemsResult.DataItems)
1858
+ {
1859
+ var valueResult = schema.GetRecipeDataItemValue(
1860
+ recipeId,
1861
+ item.ItemRelativeBrowsePath,
1862
+ item.DataItemRelativeBrowsePath,
1863
+ item.ElementAccess);
1864
+
1865
+ if (valueResult.ResultCode != GetRecipeDataItemValueResultCode.Success)
1866
+ {
1867
+ Log.Warning($"Could not retrieve value for {string.Join("/", item.ItemRelativeBrowsePath)}");
1868
+ allValid = false;
1869
+ }
1870
+ }
1871
+
1872
+ // Validate metadata
1873
+ var metaResult = schema.GetRecipeMetadataValues(recipeId);
1874
+ if (metaResult.ResultCode != GetRecipeMetadataValuesResultCode.Success)
1875
+ {
1876
+ Log.Warning("Could not retrieve metadata");
1877
+ allValid = false;
1878
+ }
1879
+
1880
+ return allValid;
1881
+ }
1882
+ ```
1883
+
1884
+ ### Delete Recipe with Confirmation
1885
+
1886
+ This method implements a **two-step confirmation pattern** to prevent accidental deletion. It is designed to be called from a UI button via `ExportMethod`:
1887
+
1888
+ 1. **First call** (`confirmed = false`, the default): performs a dry run. It looks up the recipe and logs the details of what *would* be deleted, then returns without making any changes. This gives the operator a chance to review before committing.
1889
+ 2. **Second call** (`confirmed = true`): performs the actual deletion.
1890
+
1891
+ This pattern is useful in HMI screens where a "Delete" button can first show a confirmation prompt (`confirmed` parameter set to `false`) and only proceed when the operator explicitly confirms the action (`confirmed` parameter set to `true`).
1892
+
1893
+ ```csharp
1894
+ [ExportMethod]
1895
+ public void DeleteRecipeWithConfirmation(string recipeName, string version, bool confirmed)
1896
+ {
1897
+ var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
1898
+ if (schema == null)
1899
+ return;
1900
+
1901
+ var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = version };
1902
+
1903
+ if (!confirmed)
1904
+ {
1905
+ // First call: log what would be deleted
1906
+ var result = schema.GetRecipes();
1907
+ if (result.ResultCode == GetRecipesResultCode.Success)
1908
+ {
1909
+ var recipe = result.Recipes.FirstOrDefault(r =>
1910
+ r.FTOptix.RecipeX.RecipeId.Name == recipeName &&
1911
+ r.FTOptix.RecipeX.RecipeId.Version == version);
1912
+
1913
+ if (recipe != null)
1914
+ {
1915
+ Log.Info($"Would delete recipe: {recipe.FTOptix.RecipeX.RecipeId.Name} v{recipe.FTOptix.RecipeX.RecipeId.Version} " +
1916
+ $"(modified: {recipe.ModifiedAt})");
1917
+ return;
1918
+ }
1919
+ }
1920
+
1921
+ Log.Warning("Recipe not found");
1922
+ return;
1923
+ }
1924
+
1925
+ // Second call with confirmed=true: actually delete
1926
+ var deleteResult = schema.DeleteRecipe(recipeId);
1927
+
1928
+ if (deleteResult == DeleteRecipeResultCode.Success)
1929
+ {
1930
+ Log.Info($"Recipe '{recipeName}' deleted successfully");
1931
+ }
1932
+ else
1933
+ {
1934
+ Log.Error($"Failed to delete recipe: {deleteResult}");
1935
+ }
1936
+ }
1937
+ ```
1938
+
1939
+ ### Edit Recipe (Edit Model Workflow)
1940
+
1941
+ Complete workflow for editing a recipe using edit models:
1942
+
1943
+ > [!NOTE]
1944
+ > The Edit Model session is tied to the lifecycle of its **parent node** (`parentNodeId` passed to `CreateEditModel`). If the parent node is deleted, the Edit Model node created underneath it is automatically deleted as well. This means you can scope Edit Model cleanup to session or screen lifecycle by controlling the parent node's lifetime rather than managing the Edit Model node directly.
1945
+
1946
+ ```csharp
1947
+ [ExportMethod]
1948
+ public NodeId StartEditingRecipe(string recipeName, string version)
1949
+ {
1950
+ var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
1951
+ var parentNode = LogicObject.Owner.GetVariable("EditModelsParent").Value;
1952
+ var requester = LogicObject.GetNodeId();
1953
+ var target = LogicObject.Owner.GetVariable("TargetObject").Value;
1954
+
1955
+ if (schema == null)
1956
+ return NodeId.Empty;
1957
+
1958
+ var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = version };
1959
+
1960
+ // Create edit model
1961
+ var createResult = schema.CreateEditModel(parentNode, requester, target, recipeId);
1962
+
1963
+ if (createResult.ResultCode != CreateEditModelResultCode.Success)
1964
+ {
1965
+ Log.Error($"Failed to create edit model: {createResult.ResultCode}");
1966
+ return NodeId.Empty;
1967
+ }
1968
+
1969
+ // Load recipe into edit model
1970
+ var transferResult = schema.TransferFromStoreToEditModel(requester, recipeId);
1971
+
1972
+ if (transferResult != FTOptix.RecipeX.TransferFromStoreToEditModelResultCode.Success)
1973
+ {
1974
+ Log.Error($"Failed to load recipe into edit model: {transferResult}");
1975
+ return NodeId.Empty;
1976
+ }
1977
+
1978
+ // Cast to FTOptix.RecipeX.EditModel to access additional members such as PendingChanges
1979
+ // and transfer methods that operate directly on this edit model instance.
1980
+ var editModel = (FTOptix.RecipeX.EditModel)InformationModel.Get(createResult.EditModelNodeId);
1981
+ Log.Info($"Recipe '{recipeName}' loaded for editing. Pending changes: {editModel.PendingChanges}");
1982
+ return createResult.EditModelNodeId;
1983
+ }
1984
+
1985
+ [ExportMethod]
1986
+ public void CancelEditingRecipe(NodeId editModelId)
1987
+ {
1988
+ // Simply delete the edit model
1989
+ var editModel = InformationModel.Get(editModelId);
1990
+ if (editModel != null)
1991
+ {
1992
+ editModel.Delete();
1993
+ Log.Info("Edit model discarded");
1994
+ }
1995
+ }
1996
+
1997
+ [ExportMethod]
1998
+ public void SaveEditedRecipe(NodeId editModelId, string recipeName, string version,
1999
+ NodeId requesterNodeId)
2000
+ {
2001
+ var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
2002
+ if (schema == null)
2003
+ return;
2004
+
2005
+ var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = version };
2006
+
2007
+ // Save changes back to store
2008
+ var result = schema.TransferFromEditModelToStore(requesterNodeId, recipeId);
2009
+
2010
+ if (result == FTOptix.RecipeX.TransferFromEditModelToStoreResultCode.SuccessRecipeUpdated ||
2011
+ result == FTOptix.RecipeX.TransferFromEditModelToStoreResultCode.SuccessRecipeCreated)
2012
+ {
2013
+ Log.Info($"Recipe '{recipeName}' saved successfully");
2014
+
2015
+ // Clean up edit model
2016
+ var editModel = InformationModel.Get(editModelId);
2017
+ if (editModel != null)
2018
+ editModel.Delete();
2019
+ }
2020
+ else
2021
+ {
2022
+ Log.Error($"Failed to save recipe: {result}");
2023
+ }
2024
+ }
2025
+
2026
+ [ExportMethod]
2027
+ public void ApplyAndClose(NodeId editModelId, NodeId targetNodeId, string recipeName,
2028
+ NodeId requesterNodeId, FTOptix.RecipeX.ErrorPolicy errorPolicy = FTOptix.RecipeX.ErrorPolicy.Strict)
2029
+ {
2030
+ var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
2031
+ if (schema == null)
2032
+ return;
2033
+
2034
+ // Apply edit model directly to target
2035
+ var applyResult = schema.TransferFromEditModelToTarget(requesterNodeId, targetNodeId, errorPolicy);
2036
+
2037
+ if (applyResult == FTOptix.RecipeX.TransferFromEditModelToTargetResultCode.Success)
2038
+ {
2039
+ Log.Info($"Recipe '{recipeName}' applied to target");
2040
+
2041
+ // Clean up
2042
+ var editModel = InformationModel.Get(editModelId);
2043
+ if (editModel != null)
2044
+ editModel.Delete();
2045
+ }
2046
+ else
2047
+ {
2048
+ Log.Error($"Failed to apply recipe to target: {applyResult}");
2049
+ }
2050
+ }
2051
+ ```
2052
+
2053
+ ### Accessing an Edit Model Managed by a ListView
2054
+
2055
+ When the **Recipes Editor** widget from the TemplateLibrary is used, FactoryTalk Optix automatically creates an `FTOptix.RecipeX.EditModel` to back the `ListView`. This edit model is not exposed as a visible project node, it exists internally and is not directly accessible as children of the `ListView` instance. To programmatically obtain a reference to it, call `CreateEditModel` passing the `ListView.NodeId` as both `parentNodeId` and `requesterNodeId`. Because the edit model already exists for that parent/requester pair, the call returns the existing instance rather than creating a new one.
2056
+
2057
+ This pattern is useful when you want to run server-side logic against the values the user is currently editing, for example to validate ranges or cross-field constraints before the recipe is saved.
2058
+
2059
+ ```csharp
2060
+ [ExportMethod]
2061
+ public void ValidateListViewRecipe(NodeId listViewNodeId, out bool isValidRecipe)
2062
+ {
2063
+ isValidRecipe = true;
2064
+
2065
+ var listView = InformationModel.Get<ListView>(listViewNodeId);
2066
+ if (listView == null)
2067
+ {
2068
+ Log.Error("ListView not found");
2069
+ isValidRecipe = false;
2070
+ return;
2071
+ }
2072
+
2073
+ var recipeSchema = Project.Current.Get<FTOptix.RecipeX.RecipeSchema>("Recipes/RecipeSchema1");
2074
+ if (recipeSchema == null)
2075
+ {
2076
+ Log.Error("FTOptix.RecipeX.RecipeSchema not found");
2077
+ isValidRecipe = false;
2078
+ return;
2079
+ }
2080
+
2081
+ // Use get-or-create: if the ListView already owns an edit model this returns it,
2082
+ // otherwise a new one is created. Pass NodeId.Empty and an empty FTOptix.RecipeX.RecipeId so the
2083
+ // call does not load a different recipe on top of the one already active.
2084
+ var createResult = recipeSchema.CreateEditModel(
2085
+ listView.NodeId, // parentNodeId — same as the ListView used when it created the edit model
2086
+ listView.NodeId, // requesterNodeId — same as the ListView used when it created the edit model
2087
+ NodeId.Empty, // targetNodeId — not needed; we are accessing an existing model
2088
+ new FTOptix.RecipeX.RecipeId()); // recipeId — empty; the loaded recipe is already in the model
2089
+
2090
+ if (createResult.ResultCode != CreateEditModelResultCode.Success)
2091
+ {
2092
+ Log.Error($"Failed to access edit model: {createResult.ResultCode}");
2093
+ isValidRecipe = false;
2094
+ return;
2095
+ }
2096
+
2097
+ var editModel = (FTOptix.RecipeX.EditModel)InformationModel.Get(createResult.EditModelNodeId);
2098
+ if (editModel == null)
2099
+ {
2100
+ Log.Error("FTOptix.RecipeX.EditModel not found");
2101
+ isValidRecipe = false;
2102
+ return;
2103
+ }
2104
+
2105
+ // Retrieve the data items defined in the recipe that is currently loaded
2106
+ var dataItemsResult = recipeSchema.GetDataItems(editModel.RecipeId);
2107
+ if (dataItemsResult.ResultCode != GetDataItemsResultCode.Success)
2108
+ {
2109
+ Log.Error($"Failed to retrieve data items: {dataItemsResult.ResultCode}");
2110
+ isValidRecipe = false;
2111
+ return;
2112
+ }
2113
+
2114
+ // Iterate every field and apply validation rules
2115
+ foreach (var dataItem in dataItemsResult.DataItems)
2116
+ {
2117
+ var valueResult = recipeSchema.GetRecipeDataItemValue(
2118
+ editModel.RecipeId,
2119
+ dataItem.ItemRelativeBrowsePath,
2120
+ dataItem.DataItemRelativeBrowsePath,
2121
+ dataItem.ElementAccess);
2122
+
2123
+ if (valueResult.ResultCode != GetRecipeDataItemValueResultCode.Success)
2124
+ {
2125
+ Log.Warning($"Could not read '{string.Join("/", dataItem.ItemRelativeBrowsePath)}': {valueResult.ResultCode}");
2126
+ isValidRecipe = false;
2127
+ continue;
2128
+ }
2129
+
2130
+ var value = valueResult.DataItemValue;
2131
+ var fieldName = string.Join("/", dataItem.ItemRelativeBrowsePath);
2132
+
2133
+ if (value == null)
2134
+ {
2135
+ Log.Warning($"Field '{fieldName}' has a null value");
2136
+ isValidRecipe = false;
2137
+ continue;
2138
+ }
2139
+
2140
+ // Add your custom validation logic here.
2141
+ // Examples:
2142
+ // if (value is double d && (d < 0 || d > 100)) { ... isValidRecipe = false; }
2143
+ // if (fieldName == "Motor/MaxSpeed" && (int)value > 3000) { ... isValidRecipe = false; }
2144
+ Log.Info($"Field '{fieldName}' = {value} — OK");
2145
+ }
2146
+
2147
+ Log.Info(isValidRecipe
2148
+ ? $"Recipe '{editModel.RecipeId.Name}' passed validation"
2149
+ : $"Recipe '{editModel.RecipeId.Name}' failed validation");
2150
+ }
2151
+ ```
2152
+
2153
+ > [!NOTE]
2154
+ > Do not delete or commit the edit model inside this method. Lifecycle management of the edit model (saving, discarding) belongs to the ListView. This method is read-only with respect to the edit model — it only inspects the current values.
2155
+
2156
+ ### Validating Edit Model Values Before Save
2157
+
2158
+ The example above reads the recipe values from the **store** via `RecipeSchema.GetRecipeDataItemValue`, which does not reflect changes the user has typed into a `ListView` but not yet saved. To validate the **live, unsaved values** currently held by the Edit Model, use `EditModel.GetDataItemValue` instead. This is the recommended check to run just before calling `TransferFromEditModelToStore` or `TransferFromEditModelToTarget`, so invalid input can be rejected before it is persisted or applied.
2159
+
2160
+ Key differences from `RecipeSchema.GetRecipeDataItemValue`:
2161
+ - It is called on the resolved `FTOptix.RecipeX.EditModel` instance, not on the `RecipeSchema`.
2162
+ - It takes the resolved **item node** (`NodeId`) rather than `ItemRelativeBrowsePath` — resolve it once via `editModel.TargetNodeId` and `IUANode.Get(itemRelativePath)`.
2163
+ - It returns the value currently in the Edit Model, including any pending, unsaved edits.
2164
+
2165
+ ```csharp
2166
+ [ExportMethod]
2167
+ public void ValidateEditedParameterRange(double minValue, double maxValue, out bool isValidRecipe, out string validationMessage)
2168
+ {
2169
+ isValidRecipe = false;
2170
+ validationMessage = string.Empty;
2171
+
2172
+ if (minValue > maxValue)
2173
+ {
2174
+ SetFailure("Minimum value cannot be greater than maximum value.", ref isValidRecipe, ref validationMessage);
2175
+ return;
2176
+ }
2177
+
2178
+ var parametersListView = Owner?.Owner?.Owner?.Get<ListView>("RecipeParameters/ParametersListView");
2179
+ if (parametersListView == null)
2180
+ {
2181
+ SetFailure("Recipe parameters ListView was not found.", ref isValidRecipe, ref validationMessage);
2182
+ return;
2183
+ }
2184
+
2185
+ var recipeSchema = Project.Current.Get<FTOptix.RecipeX.RecipeSchema>("Recipes/RecipeSchema1");
2186
+ if (recipeSchema == null)
2187
+ {
2188
+ SetFailure("Recipe schema not found at 'Recipes/RecipeSchema1'.", ref isValidRecipe, ref validationMessage);
2189
+ return;
2190
+ }
2191
+
2192
+ // Get-or-create: accesses the edit model already owned by the ListView
2193
+ var createResult = recipeSchema.CreateEditModel(
2194
+ parametersListView.NodeId,
2195
+ parametersListView.NodeId,
2196
+ NodeId.Empty,
2197
+ new FTOptix.RecipeX.RecipeId { Name = string.Empty, Version = string.Empty });
2198
+
2199
+ if (createResult.ResultCode != FTOptix.RecipeX.CreateEditModelResultCode.Success)
2200
+ {
2201
+ SetFailure($"Failed to access ListView edit model: {createResult.ResultCode}.", ref isValidRecipe, ref validationMessage);
2202
+ return;
2203
+ }
2204
+
2205
+ var editModel = InformationModel.Get<FTOptix.RecipeX.EditModel>(createResult.EditModelNodeId);
2206
+ var targetNode = InformationModel.Get(editModel.TargetNodeId);
2207
+ if (editModel == null || targetNode == null)
2208
+ {
2209
+ SetFailure("Edit model or its target node could not be resolved.", ref isValidRecipe, ref validationMessage);
2210
+ return;
2211
+ }
2212
+
2213
+ var dataItemsResult = recipeSchema.GetDataItems(editModel.FTOptix.RecipeX.RecipeId);
2214
+ if (dataItemsResult.ResultCode != FTOptix.RecipeX.GetDataItemsResultCode.Success || dataItemsResult.DataItems.Length == 0)
2215
+ {
2216
+ SetFailure($"Unable to retrieve recipe data items: {dataItemsResult.ResultCode}.", ref isValidRecipe, ref validationMessage);
2217
+ return;
2218
+ }
2219
+
2220
+ var invalidFields = 0;
2221
+
2222
+ foreach (var dataItem in dataItemsResult.DataItems)
2223
+ {
2224
+ var itemRelativePath = string.Join("/", dataItem.ItemRelativeBrowsePath ?? Array.Empty<string>());
2225
+ var fullPath = string.Join("/", (dataItem.ItemRelativeBrowsePath ?? Array.Empty<string>())
2226
+ .Concat(dataItem.DataItemRelativeBrowsePath ?? Array.Empty<string>()));
2227
+
2228
+ // Resolve the item node reached by ItemRelativeBrowsePath from the Edit Model's target
2229
+ var itemNodeId = targetNode.NodeId;
2230
+ if (!string.IsNullOrWhiteSpace(itemRelativePath))
2231
+ {
2232
+ var itemNode = targetNode.Get(itemRelativePath);
2233
+ if (itemNode == null)
2234
+ {
2235
+ invalidFields++;
2236
+ Log.Warning("RecipeValidation", $"Unable to resolve item node for '{itemRelativePath}'.");
2237
+ continue;
2238
+ }
2239
+ itemNodeId = itemNode.NodeId;
2240
+ }
2241
+
2242
+ // Read the current, unsaved value directly from the Edit Model
2243
+ var valueResult = editModel.GetDataItemValue(
2244
+ itemNodeId,
2245
+ dataItem.DataItemRelativeBrowsePath,
2246
+ dataItem.ElementAccess,
2247
+ dataItem.DataTypeId);
2248
+
2249
+ if (valueResult.ResultCode != FTOptix.RecipeX.GetDataItemValueResultCode.Success || valueResult.DataItemValue == null)
2250
+ {
2251
+ invalidFields++;
2252
+ Log.Warning("RecipeValidation", $"Unable to read edited value for '{fullPath}': {valueResult.ResultCode}");
2253
+ continue;
2254
+ }
2255
+
2256
+ if (!TryConvertToDouble(valueResult.DataItemValue, out var numericValue) || numericValue < minValue || numericValue > maxValue)
2257
+ {
2258
+ invalidFields++;
2259
+ Log.Warning("RecipeValidation", $"Edited value for '{fullPath}' is out of range [{minValue}, {maxValue}].");
2260
+ continue;
2261
+ }
2262
+ }
2263
+
2264
+ isValidRecipe = invalidFields == 0;
2265
+ validationMessage = isValidRecipe
2266
+ ? "Validation passed: all fields are within range."
2267
+ : $"Validation failed: {invalidFields} of {dataItemsResult.DataItems.Length} fields are invalid or out of range.";
2268
+ }
2269
+
2270
+ private void SetFailure(string message, ref bool isValidRecipe, ref string validationMessage)
2271
+ {
2272
+ isValidRecipe = false;
2273
+ validationMessage = message;
2274
+ Log.Warning("RecipeValidation", message);
2275
+ }
2276
+
2277
+ // Converts numeric-like UAValue/CLR types to double for range comparison; returns false for non-numeric values
2278
+ private static bool TryConvertToDouble(object value, out double numericValue)
2279
+ {
2280
+ if (value is UAValue uaValue)
2281
+ value = uaValue.Value;
2282
+
2283
+ switch (value)
2284
+ {
2285
+ case double d: numericValue = d; return true;
2286
+ case float f: numericValue = f; return true;
2287
+ case int i: numericValue = i; return true;
2288
+ case long l: numericValue = l; return true;
2289
+ case string str when double.TryParse(str, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed):
2290
+ numericValue = parsed;
2291
+ return true;
2292
+ default:
2293
+ numericValue = 0;
2294
+ return false;
2295
+ }
2296
+ }
2297
+ ```
2298
+
2299
+ > [!TIP]
2300
+ > Because `GetDataItemValue` reads directly from the Edit Model, this pattern lets you reject invalid input (e.g. out-of-range values) before calling `TransferFromEditModelToStore`/`TransferFromEditModelToTarget`, preventing bad data from ever reaching the store or the running target.
2301
+
2302
+ ### Bulk Recipe Operations
2303
+
2304
+ Perform operations on multiple recipes:
2305
+
2306
+ ```csharp
2307
+ [ExportMethod]
2308
+ public void RenameAllRecipesWithPrefix(string oldPrefix, string newPrefix)
2309
+ {
2310
+ var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
2311
+ if (schema == null)
2312
+ return;
2313
+
2314
+ var recipesResult = schema.GetRecipes();
2315
+ if (recipesResult.ResultCode != GetRecipesResultCode.Success)
2316
+ {
2317
+ Log.Error("Failed to retrieve recipes");
2318
+ return;
2319
+ }
2320
+
2321
+ int renamed = 0;
2322
+ foreach (var recipe in recipesResult.Recipes)
2323
+ {
2324
+ if (recipe.RecipeId.Name.StartsWith(oldPrefix))
2325
+ {
2326
+ var newName = recipe.RecipeId.Name.Replace(oldPrefix, newPrefix);
2327
+ var result = schema.RenameRecipe(recipe.RecipeId, newName);
2328
+
2329
+ if (result.ResultCode == RenameRecipeResultCode.Success)
2330
+ {
2331
+ renamed++;
2332
+ Log.Info($"Renamed: {recipe.RecipeId.Name} -> {newName}");
2333
+ }
2334
+ }
2335
+ }
2336
+
2337
+ Log.Info($"Renamed {renamed} recipes");
2338
+ }
2339
+
2340
+ [ExportMethod]
2341
+ public void DeleteRecipesOlderThan(int daysOld)
2342
+ {
2343
+ var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
2344
+ if (schema == null)
2345
+ return;
2346
+
2347
+ var recipesResult = schema.GetRecipes();
2348
+ if (recipesResult.ResultCode != GetRecipesResultCode.Success)
2349
+ return;
2350
+
2351
+ var cutoffDate = DateTime.UtcNow.AddDays(-daysOld);
2352
+ int deleted = 0;
2353
+
2354
+ foreach (var recipe in recipesResult.Recipes)
2355
+ {
2356
+ if (recipe.ModifiedAt < cutoffDate)
2357
+ {
2358
+ var result = schema.DeleteRecipe(recipe.FTOptix.RecipeX.RecipeId);
2359
+ if (result == DeleteRecipeResultCode.Success)
2360
+ {
2361
+ deleted++;
2362
+ Log.Info($"Deleted old recipe: {recipe.FTOptix.RecipeX.RecipeId.Name}");
2363
+ }
2364
+ }
2365
+ }
2366
+
2367
+ Log.Info($"Deleted {deleted} old recipes");
2368
+ }
2369
+ ```
2370
+
2371
+ ## Important Considerations
2372
+
2373
+ ### Schema Compatibility
2374
+
2375
+ - Legacy recipe schemas and new RecipeX schemas can coexist in the same project but they have different structures, so they cannot easily interoperate.
2376
+ - Recipe parameters represent Items or DataItems selected in the FTOptix.RecipeX.RecipeSchema editor.
2377
+ - Recipe parameters displayed in a ListView are always presented as read-only.
2378
+
2379
+ ### Error Handling Best Practices
2380
+
2381
+ - Always check result codes after operations; they indicate specific error conditions.
2382
+ - Use `FTOptix.RecipeX.ErrorPolicy.Strict` for critical operations and `FTOptix.RecipeX.ErrorPolicy.BestEffort` for optional operations.
2383
+ - Log failures with the specific result code for easier diagnosis.