596 lines
26 KiB
C#
596 lines
26 KiB
C#
using DocumentFormat.OpenXml;
|
|
using DocumentFormat.OpenXml.Packaging;
|
|
using DocumentFormat.OpenXml.Wordprocessing;
|
|
|
|
namespace MiniMaxAIDocx.Core.Samples;
|
|
|
|
/// <summary>
|
|
/// Reference implementations for revision tracking (Track Changes).
|
|
///
|
|
/// ╔══════════════════════════════════════════════════════════════════╗
|
|
/// ║ CRITICAL: w:del uses w:delText, NEVER w:t ║
|
|
/// ║ w:ins uses w:t, NEVER w:delText ║
|
|
/// ║ Getting this wrong silently corrupts the document. ║
|
|
/// ║ Word will open without error but display garbled text or ║
|
|
/// ║ lose content when accepting/rejecting changes. ║
|
|
/// ╚══════════════════════════════════════════════════════════════════╝
|
|
///
|
|
/// KEY CONCEPTS:
|
|
/// - Every revision element (ins, del, rPrChange, pPrChange) needs:
|
|
/// w:id — unique revision ID (string, must be unique across all revisions)
|
|
/// w:author — who made the change
|
|
/// w:date — ISO 8601 timestamp
|
|
/// - InsertedRun (w:ins) wraps normal Run elements with w:t text
|
|
/// - DeletedRun (w:del) wraps Run elements that use DeletedText (w:delText) instead of Text (w:t)
|
|
/// - MoveFrom/MoveTo track text that was moved (not just deleted+inserted)
|
|
/// </summary>
|
|
public static class TrackChangesSamples
|
|
{
|
|
/// <summary>
|
|
/// Thread-safe counter for generating unique revision IDs.
|
|
/// In production, scan the document for the max existing ID first.
|
|
/// </summary>
|
|
private static int s_revisionCounter;
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 1. EnableTrackChanges
|
|
// ──────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Enables revision tracking in the document settings.
|
|
/// This makes Word record all subsequent edits as tracked changes.
|
|
///
|
|
/// Maps to: <w:trackChanges/> in settings.xml
|
|
///
|
|
/// Note: This only controls whether NEW edits are tracked.
|
|
/// Existing revision marks are always preserved regardless of this setting.
|
|
/// </summary>
|
|
public static void EnableTrackChanges(DocumentSettingsPart settingsPart)
|
|
{
|
|
settingsPart.Settings ??= new Settings();
|
|
|
|
var existing = settingsPart.Settings.GetFirstChild<TrackRevisions>();
|
|
if (existing == null)
|
|
{
|
|
settingsPart.Settings.Append(new TrackRevisions());
|
|
}
|
|
|
|
settingsPart.Settings.Save();
|
|
}
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 2. InsertTrackedInsertion — w:ins with w:t
|
|
// ──────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Inserts text as a tracked insertion (w:ins).
|
|
///
|
|
/// ╔══════════════════════════════════════════════════════╗
|
|
/// ║ w:ins uses w:t (Text), NOT w:delText. ║
|
|
/// ║ The text appears with green underline in Word. ║
|
|
/// ╚══════════════════════════════════════════════════════╝
|
|
///
|
|
/// XML structure:
|
|
/// <w:ins w:id="1" w:author="John" w:date="2026-03-22T00:00:00Z">
|
|
/// <w:r>
|
|
/// <w:t>inserted text</w:t> <!-- w:t, NOT w:delText -->
|
|
/// </w:r>
|
|
/// </w:ins>
|
|
/// </summary>
|
|
public static InsertedRun InsertTrackedInsertion(Paragraph para, string text, string author)
|
|
{
|
|
var ins = new InsertedRun
|
|
{
|
|
Id = GenerateRevisionId(),
|
|
Author = author,
|
|
Date = DateTime.UtcNow
|
|
};
|
|
|
|
// CORRECT: w:ins contains w:r with w:t (normal Text element)
|
|
ins.Append(new Run(
|
|
new Text(text) { Space = SpaceProcessingModeValues.Preserve }));
|
|
|
|
para.Append(ins);
|
|
return ins;
|
|
}
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 3. InsertTrackedDeletion — w:del with w:delText
|
|
// ──────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Inserts text as a tracked deletion (w:del).
|
|
///
|
|
/// ╔══════════════════════════════════════════════════════╗
|
|
/// ║ w:del uses w:delText (DeletedText), NOT w:t. ║
|
|
/// ║ Using w:t inside w:del SILENTLY CORRUPTS the file. ║
|
|
/// ║ The text appears with red strikethrough in Word. ║
|
|
/// ╚══════════════════════════════════════════════════════╝
|
|
///
|
|
/// XML structure:
|
|
/// <w:del w:id="2" w:author="John" w:date="2026-03-22T00:00:00Z">
|
|
/// <w:r>
|
|
/// <w:delText xml:space="preserve">deleted text</w:delText> <!-- w:delText, NOT w:t -->
|
|
/// </w:r>
|
|
/// </w:del>
|
|
/// </summary>
|
|
public static DeletedRun InsertTrackedDeletion(Paragraph para, string deletedText, string author)
|
|
{
|
|
var del = new DeletedRun
|
|
{
|
|
Id = GenerateRevisionId(),
|
|
Author = author,
|
|
Date = DateTime.UtcNow
|
|
};
|
|
|
|
// CORRECT: w:del contains w:r with w:delText (DeletedText element)
|
|
// WRONG would be: new Text(deletedText) — this creates w:t which corrupts the document
|
|
del.Append(new Run(
|
|
new DeletedText(deletedText) { Space = SpaceProcessingModeValues.Preserve }));
|
|
|
|
para.Append(del);
|
|
return del;
|
|
}
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 4. InsertFormattingChange — RunPropertiesChange
|
|
// ──────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Records a formatting change on a run (e.g., text was made bold).
|
|
///
|
|
/// RunPropertiesChange (w:rPrChange) stores the PREVIOUS formatting.
|
|
/// The current RunProperties on the run reflects the NEW formatting.
|
|
///
|
|
/// Example: text changed from normal to bold:
|
|
/// <w:rPr>
|
|
/// <w:b/> <!-- current: bold -->
|
|
/// <w:rPrChange w:id="3" w:author="John" w:date="...">
|
|
/// <w:rPr/> <!-- previous: no bold -->
|
|
/// </w:rPrChange>
|
|
/// </w:rPr>
|
|
/// </summary>
|
|
public static void InsertFormattingChange(Run run, string author)
|
|
{
|
|
// Ensure RunProperties exists
|
|
run.RunProperties ??= new RunProperties();
|
|
|
|
// Store the previous (empty/normal) formatting as the "before" state
|
|
var rPrChange = new RunPropertiesChange
|
|
{
|
|
Id = GenerateRevisionId(),
|
|
Author = author,
|
|
Date = DateTime.UtcNow
|
|
};
|
|
|
|
// The child RunProperties inside rPrChange is the OLD formatting (before the change).
|
|
// An empty RunProperties means "was default/normal formatting."
|
|
rPrChange.Append(new PreviousRunProperties());
|
|
|
|
run.RunProperties.Append(rPrChange);
|
|
}
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 5. InsertParagraphFormatChange — ParagraphPropertiesChange
|
|
// ──────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Records a paragraph formatting change (e.g., alignment changed).
|
|
///
|
|
/// ParagraphPropertiesChange (w:pPrChange) stores the PREVIOUS paragraph properties.
|
|
/// The current ParagraphProperties reflects the NEW formatting.
|
|
///
|
|
/// Example: paragraph changed from left-aligned to centered:
|
|
/// <w:pPr>
|
|
/// <w:jc w:val="center"/> <!-- current: centered -->
|
|
/// <w:pPrChange w:id="4" w:author="John" w:date="...">
|
|
/// <w:pPr>
|
|
/// <w:jc w:val="left"/> <!-- previous: left -->
|
|
/// </w:pPr>
|
|
/// </w:pPrChange>
|
|
/// </w:pPr>
|
|
/// </summary>
|
|
public static void InsertParagraphFormatChange(Paragraph para, string author)
|
|
{
|
|
para.ParagraphProperties ??= new ParagraphProperties();
|
|
|
|
var pPrChange = new ParagraphPropertiesChange
|
|
{
|
|
Id = GenerateRevisionId(),
|
|
Author = author,
|
|
Date = DateTime.UtcNow
|
|
};
|
|
|
|
// Store previous paragraph properties (before the change)
|
|
// Example: was left-aligned before changing to whatever the current alignment is
|
|
var previousPPr = new ParagraphPropertiesExtended();
|
|
previousPPr.Append(new Justification { Val = JustificationValues.Left });
|
|
pPrChange.Append(previousPPr);
|
|
|
|
para.ParagraphProperties.Append(pPrChange);
|
|
}
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 6. InsertTableRowInsertion — table revision marks
|
|
// ──────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Marks a table row as a tracked insertion.
|
|
///
|
|
/// Table-level track changes use TableRowProperties with InsertedMathControl
|
|
/// mapped from w:trPr/w:ins — indicating the entire row was inserted.
|
|
///
|
|
/// Structure:
|
|
/// <w:tr>
|
|
/// <w:trPr>
|
|
/// <w:ins w:id="5" w:author="John" w:date="..."/>
|
|
/// </w:trPr>
|
|
/// <w:tc>...</w:tc>
|
|
/// </w:tr>
|
|
/// </summary>
|
|
public static void InsertTableRowInsertion(TableRow row, string author)
|
|
{
|
|
row.TableRowProperties ??= new TableRowProperties();
|
|
|
|
var inserted = new Inserted
|
|
{
|
|
Id = GenerateRevisionId(),
|
|
Author = author,
|
|
Date = DateTime.UtcNow
|
|
};
|
|
|
|
row.TableRowProperties.Append(inserted);
|
|
}
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 7. AcceptAllRevisions — accept all tracked changes
|
|
// ──────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Programmatically accepts all tracked changes in the document body.
|
|
///
|
|
/// For insertions (w:ins): unwrap the content (keep the runs, remove the w:ins wrapper)
|
|
/// For deletions (w:del): remove the entire element (the deleted text disappears)
|
|
/// For formatting changes: remove the rPrChange/pPrChange (keep new formatting)
|
|
/// For table row insertions: remove the w:ins from trPr
|
|
///
|
|
/// ╔══════════════════════════════════════════════════════════════╗
|
|
/// ║ Process deletions before insertions to avoid invalidating ║
|
|
/// ║ element references. Always call .ToList() before ║
|
|
/// ║ iterating to avoid modifying the collection during ║
|
|
/// ║ enumeration. ║
|
|
/// ╚══════════════════════════════════════════════════════════════╝
|
|
/// </summary>
|
|
public static void AcceptAllRevisions(Body body)
|
|
{
|
|
// 1. Accept deletions — remove the w:del and all its content
|
|
foreach (var del in body.Descendants<DeletedRun>().ToList())
|
|
{
|
|
del.Remove();
|
|
}
|
|
|
|
// 2. Accept insertions — unwrap w:ins, keeping child runs in place
|
|
foreach (var ins in body.Descendants<InsertedRun>().ToList())
|
|
{
|
|
var parent = ins.Parent;
|
|
if (parent == null) continue;
|
|
|
|
// Move all child elements before the ins element, then remove ins
|
|
var children = ins.ChildElements.ToList();
|
|
foreach (var child in children)
|
|
{
|
|
child.Remove();
|
|
ins.InsertBeforeSelf(child);
|
|
}
|
|
ins.Remove();
|
|
}
|
|
|
|
// 3. Accept formatting changes — remove rPrChange (keep new formatting)
|
|
foreach (var rPrChange in body.Descendants<RunPropertiesChange>().ToList())
|
|
{
|
|
rPrChange.Remove();
|
|
}
|
|
|
|
// 4. Accept paragraph formatting changes
|
|
foreach (var pPrChange in body.Descendants<ParagraphPropertiesChange>().ToList())
|
|
{
|
|
pPrChange.Remove();
|
|
}
|
|
|
|
// 5. Accept table row insertions — remove w:ins from trPr
|
|
foreach (var inserted in body.Descendants<TableRowProperties>()
|
|
.SelectMany(trPr => trPr.Elements<Inserted>()).ToList())
|
|
{
|
|
inserted.Remove();
|
|
}
|
|
|
|
// 6. Accept MoveFrom/MoveTo — keep MoveTo content, remove MoveFrom
|
|
foreach (var moveFrom in body.Descendants<MoveFromRun>().ToList())
|
|
{
|
|
moveFrom.Remove();
|
|
}
|
|
foreach (var moveTo in body.Descendants<MoveToRun>().ToList())
|
|
{
|
|
var parent = moveTo.Parent;
|
|
if (parent == null) continue;
|
|
var children = moveTo.ChildElements.ToList();
|
|
foreach (var child in children)
|
|
{
|
|
child.Remove();
|
|
moveTo.InsertBeforeSelf(child);
|
|
}
|
|
moveTo.Remove();
|
|
}
|
|
|
|
// 7. Remove move range markers
|
|
foreach (var marker in body.Descendants<MoveFromRangeStart>().ToList()) marker.Remove();
|
|
foreach (var marker in body.Descendants<MoveFromRangeEnd>().ToList()) marker.Remove();
|
|
foreach (var marker in body.Descendants<MoveToRangeStart>().ToList()) marker.Remove();
|
|
foreach (var marker in body.Descendants<MoveToRangeEnd>().ToList()) marker.Remove();
|
|
}
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 8. RejectAllRevisions — reject all tracked changes
|
|
// ──────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Programmatically rejects all tracked changes in the document body.
|
|
///
|
|
/// For insertions (w:ins): remove the entire element (the inserted text disappears)
|
|
/// For deletions (w:del): unwrap the content and convert w:delText back to w:t
|
|
/// (the "deleted" text is restored)
|
|
/// For formatting changes: restore old formatting from rPrChange/pPrChange
|
|
///
|
|
/// ╔══════════════════════════════════════════════════════════════╗
|
|
/// ║ When rejecting deletions, you MUST convert w:delText back ║
|
|
/// ║ to w:t. Leaving w:delText in a non-deleted run causes ║
|
|
/// ║ the text to be invisible in Word. ║
|
|
/// ╚══════════════════════════════════════════════════════════════╝
|
|
/// </summary>
|
|
public static void RejectAllRevisions(Body body)
|
|
{
|
|
// 1. Reject insertions — remove the entire w:ins and its content
|
|
foreach (var ins in body.Descendants<InsertedRun>().ToList())
|
|
{
|
|
ins.Remove();
|
|
}
|
|
|
|
// 2. Reject deletions — restore deleted text by unwrapping w:del
|
|
// and converting w:delText back to w:t
|
|
foreach (var del in body.Descendants<DeletedRun>().ToList())
|
|
{
|
|
var parent = del.Parent;
|
|
if (parent == null) continue;
|
|
|
|
// Convert DeletedText -> Text in each run inside the deletion
|
|
foreach (var run in del.Elements<Run>().ToList())
|
|
{
|
|
foreach (var delText in run.Elements<DeletedText>().ToList())
|
|
{
|
|
// IMPORTANT: convert w:delText back to w:t
|
|
var text = new Text(delText.Text ?? "") { Space = SpaceProcessingModeValues.Preserve };
|
|
delText.InsertAfterSelf(text);
|
|
delText.Remove();
|
|
}
|
|
}
|
|
|
|
// Unwrap — move children before the del element
|
|
var children = del.ChildElements.ToList();
|
|
foreach (var child in children)
|
|
{
|
|
child.Remove();
|
|
del.InsertBeforeSelf(child);
|
|
}
|
|
del.Remove();
|
|
}
|
|
|
|
// 3. Reject formatting changes — restore old RunProperties
|
|
foreach (var rPrChange in body.Descendants<RunPropertiesChange>().ToList())
|
|
{
|
|
var runProperties = rPrChange.Parent as RunProperties;
|
|
if (runProperties == null) continue;
|
|
|
|
// Get the previous (old) formatting
|
|
var previousRPr = rPrChange.GetFirstChild<PreviousRunProperties>();
|
|
if (previousRPr != null)
|
|
{
|
|
// Remove current formatting (except the rPrChange itself)
|
|
var currentProps = runProperties.ChildElements
|
|
.Where(c => c is not RunPropertiesChange).ToList();
|
|
foreach (var prop in currentProps)
|
|
{
|
|
prop.Remove();
|
|
}
|
|
|
|
// Restore old formatting from PreviousRunProperties
|
|
foreach (var oldProp in previousRPr.ChildElements.ToList())
|
|
{
|
|
oldProp.Remove();
|
|
runProperties.Append(oldProp);
|
|
}
|
|
}
|
|
rPrChange.Remove();
|
|
}
|
|
|
|
// 4. Reject paragraph formatting changes — restore old ParagraphProperties
|
|
foreach (var pPrChange in body.Descendants<ParagraphPropertiesChange>().ToList())
|
|
{
|
|
var paragraphProperties = pPrChange.Parent as ParagraphProperties;
|
|
if (paragraphProperties == null) continue;
|
|
|
|
var previousPPr = pPrChange.GetFirstChild<ParagraphPropertiesExtended>();
|
|
if (previousPPr != null)
|
|
{
|
|
var currentProps = paragraphProperties.ChildElements
|
|
.Where(c => c is not ParagraphPropertiesChange).ToList();
|
|
foreach (var prop in currentProps)
|
|
{
|
|
prop.Remove();
|
|
}
|
|
foreach (var oldProp in previousPPr.ChildElements.ToList())
|
|
{
|
|
oldProp.Remove();
|
|
paragraphProperties.Append(oldProp);
|
|
}
|
|
}
|
|
pPrChange.Remove();
|
|
}
|
|
|
|
// 5. Reject table row insertions — remove the entire row
|
|
foreach (var row in body.Descendants<TableRow>().ToList())
|
|
{
|
|
var trPr = row.TableRowProperties;
|
|
if (trPr?.GetFirstChild<Inserted>() != null)
|
|
{
|
|
row.Remove();
|
|
}
|
|
}
|
|
|
|
// 6. Reject MoveFrom/MoveTo — keep MoveFrom content (original position), remove MoveTo
|
|
foreach (var moveTo in body.Descendants<MoveToRun>().ToList())
|
|
{
|
|
moveTo.Remove();
|
|
}
|
|
foreach (var moveFrom in body.Descendants<MoveFromRun>().ToList())
|
|
{
|
|
var parent = moveFrom.Parent;
|
|
if (parent == null) continue;
|
|
|
|
// Convert any DeletedText back to Text in MoveFrom runs
|
|
foreach (var run in moveFrom.Elements<Run>().ToList())
|
|
{
|
|
foreach (var delText in run.Elements<DeletedText>().ToList())
|
|
{
|
|
var text = new Text(delText.Text ?? "") { Space = SpaceProcessingModeValues.Preserve };
|
|
delText.InsertAfterSelf(text);
|
|
delText.Remove();
|
|
}
|
|
}
|
|
|
|
var children = moveFrom.ChildElements.ToList();
|
|
foreach (var child in children)
|
|
{
|
|
child.Remove();
|
|
moveFrom.InsertBeforeSelf(child);
|
|
}
|
|
moveFrom.Remove();
|
|
}
|
|
|
|
// 7. Remove move range markers
|
|
foreach (var marker in body.Descendants<MoveFromRangeStart>().ToList()) marker.Remove();
|
|
foreach (var marker in body.Descendants<MoveFromRangeEnd>().ToList()) marker.Remove();
|
|
foreach (var marker in body.Descendants<MoveToRangeStart>().ToList()) marker.Remove();
|
|
foreach (var marker in body.Descendants<MoveToRangeEnd>().ToList()) marker.Remove();
|
|
}
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 9. InsertMoveFromTo — MoveFrom + MoveTo blocks
|
|
// ──────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Creates a tracked move operation (text moved from one location to another).
|
|
///
|
|
/// A move consists of:
|
|
/// - MoveFromRangeStart/End markers around the original location
|
|
/// - MoveFrom (w:moveFrom) containing the original text with w:delText
|
|
/// - MoveToRangeStart/End markers around the new location
|
|
/// - MoveTo (w:moveTo) containing the moved text with w:t
|
|
/// - Both share the same name attribute to link them
|
|
///
|
|
/// ╔══════════════════════════════════════════════════════════════╗
|
|
/// ║ MoveFrom uses w:delText (like w:del — text is "leaving") ║
|
|
/// ║ MoveTo uses w:t (like w:ins — text is "arriving") ║
|
|
/// ╚══════════════════════════════════════════════════════════════╝
|
|
/// </summary>
|
|
public static void InsertMoveFromTo(Body body, string movedText, string author)
|
|
{
|
|
string moveId = GenerateRevisionId();
|
|
string moveId2 = GenerateRevisionId();
|
|
string moveName = "move" + moveId;
|
|
|
|
// ── MoveFrom paragraph (original location — text shown with strikethrough) ──
|
|
var moveFromPara = new Paragraph();
|
|
|
|
moveFromPara.Append(new MoveFromRangeStart
|
|
{
|
|
Id = moveId,
|
|
Author = author,
|
|
Date = DateTime.UtcNow,
|
|
Name = moveName
|
|
});
|
|
|
|
var moveFrom = new MoveFromRun
|
|
{
|
|
Id = GenerateRevisionId(),
|
|
Author = author,
|
|
Date = DateTime.UtcNow
|
|
};
|
|
|
|
// MoveFrom uses DeletedText (w:delText), NOT Text (w:t)
|
|
// The text is visually struck through in Word
|
|
moveFrom.Append(new Run(
|
|
new DeletedText(movedText) { Space = SpaceProcessingModeValues.Preserve }));
|
|
|
|
moveFromPara.Append(moveFrom);
|
|
moveFromPara.Append(new MoveFromRangeEnd { Id = moveId });
|
|
|
|
body.Append(moveFromPara);
|
|
|
|
// ── MoveTo paragraph (destination — text shown with double underline) ──
|
|
var moveToPara = new Paragraph();
|
|
|
|
moveToPara.Append(new MoveToRangeStart
|
|
{
|
|
Id = moveId2,
|
|
Author = author,
|
|
Date = DateTime.UtcNow,
|
|
Name = moveName
|
|
});
|
|
|
|
var moveTo = new MoveToRun
|
|
{
|
|
Id = GenerateRevisionId(),
|
|
Author = author,
|
|
Date = DateTime.UtcNow
|
|
};
|
|
|
|
// MoveTo uses Text (w:t), NOT DeletedText (w:delText)
|
|
// The text is visually double-underlined in green in Word
|
|
moveTo.Append(new Run(
|
|
new Text(movedText) { Space = SpaceProcessingModeValues.Preserve }));
|
|
|
|
moveToPara.Append(moveTo);
|
|
moveToPara.Append(new MoveToRangeEnd { Id = moveId2 });
|
|
|
|
body.Append(moveToPara);
|
|
}
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 10. GenerateRevisionId — unique ID pattern
|
|
// ──────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Generates a unique revision ID string.
|
|
///
|
|
/// Revision IDs (w:id) must be unique across ALL revision elements in the document:
|
|
/// ins, del, rPrChange, pPrChange, moveFrom, moveTo, table row ins/del, etc.
|
|
///
|
|
/// Word uses simple incrementing integers starting from 0.
|
|
/// When programmatically adding revisions to an existing document,
|
|
/// first scan for the maximum existing ID and start from there.
|
|
///
|
|
/// For new documents, a simple counter suffices.
|
|
/// For existing documents, use:
|
|
/// int maxId = body.Descendants()
|
|
/// .SelectMany(e => e.GetAttributes())
|
|
/// .Where(a => a.LocalName == "id")
|
|
/// .Select(a => int.TryParse(a.Value, out int v) ? v : 0)
|
|
/// .DefaultIfEmpty(0)
|
|
/// .Max();
|
|
/// </summary>
|
|
public static string GenerateRevisionId()
|
|
{
|
|
return Interlocked.Increment(ref s_revisionCounter).ToString();
|
|
}
|
|
}
|