@* CSV tag export for a Raw-tree Device / TagGroup (B2-WP5). Reads the device's tags (via the WP5-owned RawTagCsvExportReader — no IRawTreeService extension) and renders them in the same column shape RawCsvImportModal consumes (T0-2 CsvWriter, per-driver CsvColumnMap). The CSV is offered as a self-contained data-URI download anchor (no JS interop, no external host — matches the app's CSP posture) plus a preview textarea. Modal contract (the RawTree coordinator wires this in place of the OnExportCsv stub): *@ @using System.Text @using ZB.MOM.WW.OtOpcUa.AdminUI.Uns @inject RawTagCsvExportReader ExportReader @if (Visible) { } @code { /// Whether the modal is shown (two-way bound). [Parameter] public bool Visible { get; set; } /// Raised when changes (supports @bind-Visible). [Parameter] public EventCallback VisibleChanged { get; set; } /// The device to export. May be blank when is supplied. [Parameter] public string DeviceId { get; set; } = ""; /// When set, only tags under this group's subtree are exported; null exports the whole device. [Parameter] public string? TagGroupId { get; set; } /// The device's driver-type string — selects the per-driver typed column set. [Parameter] public string DriverType { get; set; } = ""; private bool _loading; private bool _built; private string? _error; private string _csv = ""; private string _dataUri = ""; private string _fileName = "tags.csv"; private int _rowCount; private string? _groupFilter; protected override async Task OnParametersSetAsync() { if (Visible && !_built) { _built = true; await BuildAsync(); } else if (!Visible) { _built = false; } } private async Task BuildAsync() { _loading = true; _error = null; try { var deviceId = DeviceId; _groupFilter = null; if (!string.IsNullOrEmpty(TagGroupId)) { var ctx = await ExportReader.ResolveGroupContextAsync(TagGroupId!); if (ctx is { } c) { _groupFilter = c.GroupPath; if (string.IsNullOrEmpty(deviceId)) { deviceId = c.DeviceId; } } } if (string.IsNullOrEmpty(deviceId)) { _error = "Could not resolve the target device."; return; } var tags = await ExportReader.ReadDeviceTagsAsync(deviceId); // When exporting a single group, keep only that group's subtree (its path + descendants). if (!string.IsNullOrEmpty(_groupFilter)) { var prefix = _groupFilter; tags = tags .Where(t => t.TagGroupPath is not null && (t.TagGroupPath == prefix || t.TagGroupPath.StartsWith(prefix + "/", StringComparison.Ordinal))) .ToList(); } _rowCount = tags.Count; _csv = RawTagCsvMapper.Export(DriverType, tags); _fileName = BuildFileName(deviceId); var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(_csv)); _dataUri = $"data:text/csv;charset=utf-8;base64,{base64}"; } catch (Exception ex) { _error = ex.Message; } finally { _loading = false; } } private static string BuildFileName(string deviceId) { var safe = new string(deviceId.Select(c => char.IsLetterOrDigit(c) ? c : '-').ToArray()); return $"tags-{safe}.csv"; } private async Task Close() { _built = false; _csv = ""; _dataUri = ""; Visible = false; await VisibleChanged.InvokeAsync(false); } }