Home

Pushing DumpX further

I've been experimenting with the original code Joe dropped here [https://forum.linqpad.net/discussion/comment/2463#Comment_2463](Dumping in different panel#DumpX)

Specifically, adding support for a heading. Modified the signature like so:

static T DumpX<T>(T toDump, string panelName, string label = null)

Changed the formatter code to:

  if (label is not null)
    formatter.WriteLine(Util.RawHtml($"<div class=\"headingpresenter\"><h1 class=\"headingpresenter\">{label}</h1>{Util.ToHtmlString(true, toDump)}</div>"));
  else
    formatter.WriteLine(toDump);

Everything displays as expected. What isn't expected is, given an enumerable toDump, clicking on the table type header results in the following message:

The table does toggle between expanded/collapsed despite giving the dialog (kind of, the table data heading row isn't collapsed because of the error). I've also called Util.ToHtmlString(true, true, toDump) with the same behavior.

From the scripting in the page, this is the only line which references startsWith

if (table.tHead.rows.length == 2 && !table.tHead.rows[1].id.startsWith ('sum'))

It seems this is a limitation with the WebBrowser control. I've explored the officially recommended alternative, WebView2. Currently stuck on getting it initialized as doing so is an async method and calling it complains about it being on another thread.

What other options should I look at here?

Comments

  • Coincidentally I am also trying to get a way around this. Here is what I have tried so far, but none of these should be used for anything important until you have tested it.

    I tried hacking the javascript to just remove the bit that causes the error, i.e.

    browser.DocumentText = formatter.ToString()
     .Replace("!table.tHead.rows[1].id.startsWith ('sum'))", " true)" );
    

    That appears to work, but I don't know what the condition is doing, so I'm kinda expecting it to go wrong on some particular object, so not really happy with the hack.

    Also tried looking to use WebView2. This is what I have so far

        static Dictionary<string, bool> WebView2InitializationState =  new Dictionary<string, bool>();
        public static T DumpX<T>(this T toDump, string panelName)
        {
            Microsoft.Web.WebView2.WinForms.WebView2 browser;
            TextWriter formatter;
    
            var panel = PanelManager.GetOutputPanel(panelName);
            bool first = panel == null;
    
            if (first)
            {
                panel = PanelManager.DisplayControl(browser = new Microsoft.Web.WebView2.WinForms.WebView2(), panelName);
                formatter = Util.CreateXhtmlWriter(true);
                browser.Tag = formatter;
                WebView2InitializationState[panelName] = false;
                browser.CoreWebView2InitializationCompleted += (obj, args) =>
                {               
                    WebView2InitializationState[panelName] = true;
    
                    formatter = (TextWriter)browser.Tag;
                    browser.NavigateToString(formatter.ToString());
                };
                browser.EnsureCoreWebView2Async();
            }
            else
            {        
                browser =(Microsoft.Web.WebView2.WinForms.WebView2 )(panel.GetControl());
                formatter = (TextWriter)browser.Tag;
            }
    
            formatter.WriteLine(toDump);
    
            if (WebView2InitializationState[panelName] )
                 browser.NavigateToString(formatter.ToString());
    
            return toDump;
        }
    

    I have not really tested this, apart from doing a few simple dumps but it appears to work so far (except for observables but they didn't work on the old version either.)

  • Forgot to mention one issue I am aware of

    browser.EnsureCoreWebView2Async();

    WebView2 needs to create a folder with a whole loads of files and the above method means that it is created in the same folder as linqpad8.exe . I would need to figure out a suitable place to create these files and do something like

    var userDataFolder = ???? ;
    var env =   Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(null, userDataFolder).Result;
    browser.EnsureCoreWebView2Async(env);
    
  • The following methods will help:

    Util.BrowserEngine.GetWebView2ExecutableFolder()
    Util.BrowserEngine.GetWebView2DataFolder()
    

    These methods will also work if WebView2 has not been installed or is having a meltdown.

  • Thanks @JoeAlbahari

    If I use those folders, does it matter that the script is using the nuget package Microsoft.Web.WebView2 which could be a different version from the one embedded in LinqPad ?

  • It's more the version of WebView2 itself that matters. I've found that it's safest to switch to a new data folder when the WebView2 version changes, otherwise other instances still running the old version might fight the newer instances in how they write to the data folder. In any case, LINQPad looks after that, so you don't need to worry.

    The NuGet package is just a wrapper for WPF and WinForms and don't think it matters what version you use. LINQPad uses an isolated ALC to keep its own dependencies separate from your query's dependencies.

  • edited December 2023

    Based on the comments here, this is what I'm currently using. The only issue at the moment is a noticeable "flash" of white before displaying the new OutputPanel (I use dark mode).

    static Dictionary<string, bool> WebView2InitializationState =  new Dictionary<string, bool>();
    static T DumpX<T>(T toDump, string panelName, string label = null)
    {
      Microsoft.Web.WebView2.Wpf.WebView2 browser;
      TextWriter formatter;
    
      var panel = PanelManager.GetOutputPanel(panelName);
      var first = panel == null;
    
      if (first)
      {
        panel = PanelManager.DisplayWpfElement(browser = new Microsoft.Web.WebView2.Wpf.WebView2(), panelName);
        formatter = Util.CreateXhtmlWriter(true);
        browser.Tag = formatter;
        WebView2InitializationState[panelName] = false;
    
        browser.CoreWebView2InitializationCompleted += (sender, args) =>
        {
          WebView2InitializationState[panelName] = true;
    
          formatter = (TextWriter)browser.Tag;
          browser.NavigateToString(formatter.ToString());
        };
        var env = Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(Util.BrowserEngine.GetWebView2ExecutableFolder(), Util.BrowserEngine.GetWebView2DataFolder()).Result;
        browser.EnsureCoreWebView2Async(env);
      }
      else
      {
        browser = (Microsoft.Web.WebView2.Wpf.WebView2)(panel.GetWpfElement());
        formatter = (TextWriter)browser.Tag;
      }
    
      if (label is not null)
        formatter.WriteLine(Util.RawHtml($"<div class=\"headingpresenter\"><h1 class=\"headingpresenter\">{label}</h1>{Util.ToHtmlString(true, true, toDump).Dump()}</div>"));
      else
        formatter.WriteLine(Util.RawHtml(Util.ToHtmlString(true, true, toDump)));
    
      if (WebView2InitializationState[panelName])
        browser.NavigateToString(formatter.ToString());
    
      return toDump;
    }
    

    Test code

    void Main()
    {
      var response = GenerateResponseData();
    
      DumpX(response, "Set 1", "Set 1 Output");
      DumpX(response, "Set 1", "Set 2 Output");
    }
    
    Random random = new Random(1000);
    
    IEnumerable<ResponseData> GenerateResponseData()
    {
      return Enumerable
              .Repeat<int>(100000, 10)
              .Select(i => random.Next(i))
              .Select(i => new ResponseData()
              {
                Id = i,
                Name = $"Value {i}",
              });
    }
    
    class ResponseData
    {
      public int Id { get; set; }
      public string Text { get; set; }
    }
    
Sign In or Register to comment.