A small, dependency-free C# helper and 15 executable checks for a specific clipboard convention: an external payload ends its last row with CRLF, while a bare final LF can represent an intentional blank row. Copy ClipboardRows.cs and LICENSE into your project.
The motivating report was RadzenSpreadsheet #2707, about an extra row being cleared after an Excel paste. Upstream closed it with commit 091d9e5. For Radzen applications, prefer an official package that contains that change; no particular released package version is established by this guide. The standalone helper and checks below remain a reusable example of the stated row-boundary convention, not a change that every current Radzen installation still needs.
using DataShapeKit;
var rows = ClipboardRows.SplitExternalText("1\r\n2\r\n");
// ["1", "2"]
var withBlankRow = ClipboardRows.SplitExternalText("1\r\n\r\n");
// ["1", ""] — the additional blank row remains.
var lfEnding = ClipboardRows.SplitExternalText("1\n");
// ["1", ""] — a bare LF is not consumed.| Original text, shown with escapes | Returned rows |
|---|---|
1\r\n2\r\n |
1, 2 |
1\r\n\r\n |
1, empty |
1\r\n\r\n\r\n |
1, empty, empty |
1\n |
1, empty |
\r\n |
one empty row |
001\t\t\r\n |
one row containing 001 and two trailing tabs |
| empty string | one empty row, matching String.Split semantics |
Null input throws ArgumentNullException. The helper preserves spaces, tabs and string values. It does not decide whether an empty clipboard should be ignored; keep that decision in the caller.
The source paths below were inspected on 9 September 2026. Treat these as integration points to review against your version:
- In
SpreadsheetClipboard.GetPasteRange, keep the internal clipboard match and empty-input guard first. In the external-text branch, obtainlineswithClipboardRows.SplitExternalText(pastedText)instead of splitting the original text directly. - In
Worksheet.InsertDelimitedString, obtainrowswithClipboardRows.SplitExternalText(value). Keep the existing cell splitting and sheet-boundary handling. - Give both consumers the same original payload. Leave the raw text used by
TryPasteunchanged, so internal copy/cut matching retains its existing behavior.
The range calculation and insertion must agree. Changing only insertion can still make the range check include a row below the intended paste. Changing only the range can leave insertion writing farther than the checked range.
Do not call Trim, TrimEnd, remove every empty row, or remove a terminal CRLF before passing the result to this helper. Applying the removal twice can erase an intentionally blank last row. The helper removes exactly one CRLF within each call; each call must start from the original text.
Before adopting a change in Radzen, run its real command tests for an untouched cell immediately below the paste, a protected row immediately below it, a deliberately blank final row, internal copy/cut, undo, and a paste at the bottom/right edge. Those are integration checks, not claims made by this standalone suite.
The console project has no package references; NuGet.Config disables package sources. It defaults to net8.0 and requires a matching SDK and runtime:
dotnet run --project RowChecks.csprojTo select another installed target, pass CheckTargetFramework. For example, the checks were compiled and run with SDK 7.0.200 using:
dotnet run --project RowChecks.csproj --framework net7.0 -p:CheckTargetFramework=net7.0Expected output: 15/15 row-boundary cases passed. A failed check returns a nonzero exit code. Before adding the helper, the direct-split baseline passed only 4 of these 15 cases. The default .NET 8 target and the complete Radzen application have not been run here.
This is a row-boundary compatibility example for the stated CRLF convention. It is not a CSV/TSV parser: quoted multiline fields still need a quote-aware parser, and arbitrary LF-only clipboard formats may use a different final-row convention. The helper cannot infer which convention produced an arbitrary string. It does not read XLSX, validate cells, interpret formulas, or control how the receiving spreadsheet converts values.
If this saved you a little time, you're welcome to buy me a coffee. Please don't feel obliged — the code stays free, and feedback is appreciated too.
- USDC / SOL · Solana:
9tY6D9mwcFaJwwzEHvw2v7nhSpdjqjNBYtuooyBN6rYy - USDC / ETH · Base:
0x568Ab98578d682FB0B0b45619BE73EbFfbf5a6eA - USDT · BNB Smart Chain (BEP20):
0x568Ab98578d682FB0B0b45619BE73EbFfbf5a6eA
Please match the asset and network exactly. Fees depend on your wallet or exchange. Thank you! — Tevinch