.NET Core Version:
Have you experienced this same bug with .NET Framework?:
This is blocking a migration of Git Extensions to .NET Core / .NET 5.0.
Problem description:
RichTextBox control doesn't respond to EM_SETCHARFORMAT with lParam: SCF_SELECTION, wParam: CHARFORMAT2W(dwMask: CFM.LINK, dwEffects: CFE.LINK).
The text put in the RTB is as follows:
"Related links: <a href='https://github.com/gitextensions/gitextensions/commit/2c0698453e29dc243bfbc817ef74581c0cbfc830'>View on GitHub</a>, <a href='https://github.com/gitextensions/gitextensions/issues/8333'>Issue 8333</a>"



As pointed in https://github.com/dotnet/winforms/issues/3399#issuecomment-661024452 in .NET 4.7.2 there was a change of version of the underlying native control (i.e. RichEdit 4.1 from Msftedit.dll), which is probably the underlying reason of the problem.
Minimal repro:
Adoption Blocker for GitExtensions, because they depend on links in RichEdit. Can we provide an option to go back to the old one? Or we may need to update how we interact.
@dreddy-work - can you reach out to your contact from back when we switched? Ask about hidden text specifically.
Tested it locally, and unfortunately #3726 doesn't fix the issue.
Just to note, that this is blocker for NLog.Windows.Forms for moving from .Net Framework too.
I've executed the repro in the original issue in .NET 4.6.1 and it renders links. I saved the RTF markup to a file (calling SaveFile on the control) and opened it in Wordpad, which does not render the links, so I would conclude that the RTF generated is nonstandard and not supported by modern Windows. In this case there is nothing WinForms can do, the code to generate the RTF should be changed to conform to the RTF specification.
In fact, I strongly suspect the RTF generated was relying on unspecified behavior, as it looks nothing like the officially documented format for RTF hyperlinks (its linked e.g. from here in the official docs, see last paragraph). When I generate the hyperlink in the documented format it works in all versions I tested (.NET 4.6.1, .NET 4.7.2, .NET Core 3.1, .NET 5)
In summary I'd classify as "by design" and not as a bug, the native control decided to no longer support the hack used by the repro code, and this decision has been done long ago.
The linked issue in NLog.Windows.Forms on first glance seems unrelated to the original issue posted here.
snippet with code for fixing the repro
I've replaced the code for generating the RTF (from the repro from initial post). This obviously needs to be improved before using it in production code since it probably won't work if the friendly text itself requires RTF escape sequences, e.g. when containing unicode text.
private static void ProcessEndElement(XmlReader reader, RTFCurrentState cs, RichTextBox rtb)
{
switch (reader.Name.ToLower())
{
// ...
case "a":
int length = rtb.TextLength - cs.hyperlinkStart;
if (cs.hyperlink != null)
{
rtb.Select(cs.hyperlinkStart, length);
if (cs.hyperlink != rtb.SelectedText)
{
var friendlyText = rtb.SelectedText;
rtb.SelectedText = "x";
rtb.Select(cs.hyperlinkStart, 1); // Workaround: selection gets lost when writing SelectText?
string rtfText = rtb.SelectedRtf;
int idx = rtfText.LastIndexOf('}');
if (idx != -1)
{
string head = rtfText.Substring(0, idx - 1);
string tail = rtfText.Substring(idx);
RtbSetSelectedRtf(rtb, head + $@"{{{{\field{{\*\fldinst{{HYPERLINK ""{cs.hyperlink}"" }}}}{{\fldrslt{{{friendlyText}}}}}}}}}" + tail);
length = rtb.TextLength - cs.hyperlinkStart;
}
}
// reposition to final
rtb.Select(rtb.TextLength + 1, 0);
}
cs.links.Add(new KeyValuePair<int, int>(cs.hyperlinkStart, length));
cs.hyperlinkStart = -1;
break;
// ...
}
}
Instead of having users rely on RTF rewriting to create links maybe we should create an issue for designing an API exposing the SetURL method mentioned in the doc?
Reading in the RTF can work well, but it鈥檚 convenient to have a more programmatic approach. Accordingly RichEdit 6.0 added the ITextRange2::SetURL(BSTR bstr) method, which uses the bstr as the URL for the range of text selected by the ITextRange2.
Or, well, the 3rd party code in the repro which is used to interop with the RTF control could just use the API directly, since it already contains quite some interop anyways ... for an example check out ScrollToCaret which appears to use the COM API already.
While looking into NLog.Windows.Forms I noticed they use the same RTF trickery to hide a URL in hidden text and mark everything as a link - with the difference that it actually still works for them after updating their code to .NET 5 (their problem was another bug that already has been fixed but was not backported to 3.x - I did not identify the exact fix, but probably something related to hidden text, there were several fixes in that area)
Its still unclear what the difference is to the repro code from this PR, they both appear to use the same technique and one works but the other doesn't, I'll dig a bit deeper. Personally I'd still recommend just using the official hyperlink markup, but this does cause problems with the LinkClicked handler because it can no longer retrieve the URL of the clicked hyperlink. I'll open a separate issue about the LinkClicked event because its unrelated to the rendering of links discussed here.
You apparently have to set RichTextBox.DetectUrls = false; to be able to create fake links at runtime via CFE_LINK interop. Setting this property makes the repro in the original post work. Opening a separate issue for the LinkClicked handler not seeing hidden text. I think this issue can be closed?
Thank you so much!
Most helpful comment
While looking into NLog.Windows.Forms I noticed they use the same RTF trickery to hide a URL in hidden text and mark everything as a link - with the difference that it actually still works for them after updating their code to .NET 5 (their problem was another bug that already has been fixed but was not backported to 3.x - I did not identify the exact fix, but probably something related to hidden text, there were several fixes in that area)
Its still unclear what the difference is to the repro code from this PR, they both appear to use the same technique and one works but the other doesn't, I'll dig a bit deeper. Personally I'd still recommend just using the official hyperlink markup, but this does cause problems with the LinkClicked handler because it can no longer retrieve the URL of the clicked hyperlink. I'll open a separate issue about the LinkClicked event because its unrelated to the rendering of links discussed here.