MacBook, defective by design banner

title:
Put the knife down and take a green herb, dude.


descrip:

One feller's views on the state of everyday computer science & its application (and now, OTHER STUFF) who isn't rich enough to shell out for www.myfreakinfirst-andlast-name.com

Using 89% of the same design the blog had in 2001.

FOR ENTERTAINMENT PURPOSES ONLY!!!
Back-up your data and, when you bike, always wear white.

As an Amazon Associate, I earn from qualifying purchases. Affiliate links in green.

x

MarkUpDown is the best Markdown editor for professionals on Windows 10.

It includes two-pane live preview, in-app uploads to imgur for image hosting, and MultiMarkdown table support.

Features you won't find anywhere else include...

You've wasted more than $15 of your time looking for a great Markdown editor.

Stop looking. MarkUpDown is the app you're looking for.

Learn more or head over to the 'Store now!

Sunday, March 01, 2026

Okay, I hit a rabbithole today. I'm not saying it's not fun, but probably not my smartest use of time.

Long story shorter than I want, I find myself googling "SSMS equivalent on macOS" fairly often, meaning [Microsoft] SQL Server Management Studio, the Windows-specific tool you use not simply to manage SQL Servers via a GUI, but to throw raw T-SQL at it and view the results. It's barely changed since at least the 1990s.

I used to use SQuirreL-SQL on Mac a lot when I dabbled (and also worked!) in Java, which really came in handy once when my boss didn't realize we were installing a Java app dbms onto a Linux-only network, and I briefly enjoyed Azure Data Studio until it got 86'd.

Azure Data Studio was introduced as a lightweight, cross-platform tool tailored for database professionals working with SQL Server, Azure SQL Database, and other data platforms. It provided a modern interface, integrated notebooks, and extensibility through extensions. However, over time, the overlap between Azure Data Studio and Visual Studio Code became increasingly apparent. Both tools shared similar architectures, extensibility models, and even some of the same extensions.

It would probably still work great, but it's frustrating that yet another MS tool got tossed into a VS Code extension. Look, VS Code is cool, but it's not an IDE. It feels like chicken wire and duct tape on a good day. And the last thing I want to do is have MORE things living in this single scripting app. I like dedicated apps, for heaven's sake.

But you know what I've done for nearly thirty years now? Written apps that interface with SQL Server! Why don't I just write my own query client for SQL Server!

Ah, but with what UI? Originally, I figured why not just the console? In an hour or so, I got a pretty simple client up that would send queries and parse up results into columns of plain text. Not too shabby!

My introduction to TUIs

But was that enough? Of course not! We need a true console app, whatever that is, that intelligently formatted results and allowed complex SQL edits, not just line by line input.

I started by wading into Spectre Console which allows you to style console text like mad and even has some neat widgets like this table, but it also says it's not really about complex UI management:

Thank you for posting an issue, but this goes beyond the scope of what Spectre.Console is, so this is not something that we will consider. I would recommend you to take a look at something like Textual or GUI.cs

Also all its widgets are output-minded, not truly interactive. Okay, fine. Fair.

Next up was Terminal.Gui, which seems to be a modern, C#-powered take on the sort of 1980s style DOS apps that you still find in backwater DMVs and tax preparer offices (I'm seeing a TurboTax commercial running during Celtics-76ers making fun of just such a UI now). Which is awesome! Except it was immediately buggy on macOS, and even when I got that working, the first text editing widget I tried allowed text entered to bleed into the widget under it. So that's a mess. Next?

I've played with C#'s Console objects a bit before. How tough can writing my own TUI views from scratch be?

Insanely. It's insanely tough. I've copiloted the heck out of this today, and, you know, it turns out writing a text editor in a console is a pita.

The toughest part? You have to manage all the scrolling. And since you're in a console, you lose all the built in cursor movement... if you want to insert instead of overwrite, that takes some work. And if you want to go to the end of the line, well, the macOS Terminal eats command-right arrow (which means "end") and uses that combo as a shortcut to go to the next terminal window instead.

Solutions? Well, rather than reinvent the wheel, I find that readline on Linux has a lot of solutions. Here are some good ones:

  • C-b
    • Move back one character.
  • C-f
    • Move forward one character.
  • DEL or Backspace
    • Delete the character to the left of the cursor.
  • C-d
    • Delete the character underneath the cursor.
  • C-a
    • Move to the start of the line.
  • C-e
    • Move to the end of the line.
  • M-f
    • Move forward a word, where a word is composed of letters and digits.
  • M-b
    • Move backward a word.

I'm starting to see why vi was born. Though apparently I'm also learning Emacs now too (the above shortcuts are the same in Emacs, maybe?).

So throw those in. Awesome! A little esoteric, but that'll do, pig.

Except the Meta ones aren't working...

Guess what?!

Why that happens

  • On macOS Option by default produces alternate characters (Option+B โ†’ โˆซ on many layouts).
  • Terminal.app only treats Option as Meta if you enable "Use Option as Meta key" (or remap the key). Otherwise the terminal sends the Unicode character.
  • .NET's Console reports what the terminal sends: a character with no ConsoleModifiers in this case.

How to get Meta-M (M-b) behavior instead

  • Terminal.app โ†’ Preferences โ†’ Profiles โ†’ Keyboard โ†’ check "Use Option as Meta key" (or add a custom mapping to send ESC+b).

[thanks GPT-5 mini]

And there you have it. That's a lot learned today. Not sure I needed the history lesson or to write a scrollable text view for a rich console app, but I've taken on both, nonetheless.

Labels: , , ,


posted by ruffin at 3/01/2026 10:09:00 PM
Tuesday, June 17, 2025

Okay, was looking back through WCF earlier this week for a prospective client, and figured I'd leave some notes.

Here's the stock code, give or take:

public interface IService
{
    [OperationContract]
    string GetData(int value);
    // ...
}
public class Service : IService
{
    public string GetData(int value)
    {
    	return string.Format("You entered: {0}", value);
    }

    // ...
}

And all that's in Service.svc is <%@ ServiceHost Language="C#" Debug="true" Service="Service" CodeBehind="~/App_Code/Service.cs" %>

Again, this is the stock WCF project so far. Not overly fancy.

I wanted to be able to hit the stock endpoint from a WCF project in Visual Studio without using a WCF client. Getting to the WSDL file was easy: You can steal that from the page the WCF Test Client refers to if nothing else.

For me, that stock page was:

http://localhost:60817/Service.svc

Here's some of the startup page's contents:

You have created a service.

To test this service, you will need to create a client and use it to call the service. You can do this using the svcutil.exe tool from the command line with the following syntax:

svcutil.exe http://localhost:60817/Service.svc?wsdl

You can also access the service description as a single file:

http://localhost:60817/Service.svc?singleWsdl

Then the WSDL itself:

http://localhost:60817/Service.svc?wsdl

<?xml version="1.0" encoding="utf-8"?>
<wsdl:definitions name="Service" targetNamespace="http://tempuri.org/"
	xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
	xmlns:wsx="http://schemas.xmlsoap.org/ws/2004/09/mex"
	xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
	xmlns:wsa10="http://www.w3.org/2005/08/addressing"
	xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"
	xmlns:wsap="http://schemas.xmlsoap.org/ws/2004/08/addressing/policy"
	xmlns:msc="http://schemas.microsoft.com/ws/2005/12/wsdl/contract"
	xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/"
	xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
	xmlns:wsam="http://www.w3.org/2007/05/addressing/metadata"
	xmlns:xsd="http://www.w3.org/2001/XMLSchema"
	xmlns:tns="http://tempuri.org/"
	xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
	xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl"
	xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
	<wsdl:types>
		<xsd:schema targetNamespace="http://tempuri.org/Imports">
			<xsd:import schemaLocation="http://localhost:60817/Service.svc?xsd=xsd0" namespace="http://tempuri.org/"/>
			<xsd:import schemaLocation="http://localhost:60817/Service.svc?xsd=xsd1" namespace="http://schemas.microsoft.com/2003/10/Serialization/"/>
			<xsd:import schemaLocation="http://localhost:60817/Service.svc?xsd=xsd2" namespace="http://schemas.datacontract.org/2004/07/"/>
		</xsd:schema>
	</wsdl:types>
	<wsdl:message name="IService_GetData_InputMessage">
		<wsdl:part name="parameters" element="tns:GetData"/>
	</wsdl:message>
	<wsdl:message name="IService_GetData_OutputMessage">
		<wsdl:part name="parameters" element="tns:GetDataResponse"/>
	</wsdl:message>
	<wsdl:message name="IService_GetDataUsingDataContract_InputMessage">
		<wsdl:part name="parameters" element="tns:GetDataUsingDataContract"/>
	</wsdl:message>
	<wsdl:message name="IService_GetDataUsingDataContract_OutputMessage">
		<wsdl:part name="parameters" element="tns:GetDataUsingDataContractResponse"/>
	</wsdl:message>
	<wsdl:portType name="IService">
		<wsdl:operation name="GetData">
			<wsdl:input wsaw:Action="http://tempuri.org/IService/GetData" message="tns:IService_GetData_InputMessage"/>
			<wsdl:output wsaw:Action="http://tempuri.org/IService/GetDataResponse" message="tns:IService_GetData_OutputMessage"/>
		</wsdl:operation>
		<wsdl:operation name="GetDataUsingDataContract">
			<wsdl:input wsaw:Action="http://tempuri.org/IService/GetDataUsingDataContract" message="tns:IService_GetDataUsingDataContract_InputMessage"/>
			<wsdl:output wsaw:Action="http://tempuri.org/IService/GetDataUsingDataContractResponse" message="tns:IService_GetDataUsingDataContract_OutputMessage"/>
		</wsdl:operation>
	</wsdl:portType>
	<wsdl:binding name="BasicHttpBinding_IService" type="tns:IService">
		<soap:binding transport="http://schemas.xmlsoap.org/soap/http"/>
		<wsdl:operation name="GetData">
<!-- ================================================================================= -->
<!-- This part is reasonably important later.                                                     -->
<!-- ================================================================================= -->
<soap:operation soapAction="http://tempuri.org/IService/GetData" style="document"/>
<!-- ================================================================================= -->
<!-- ================================================================================= -->
			<wsdl:input>
				<soap:body use="literal"/>
			</wsdl:input>
			<wsdl:output>
				<soap:body use="literal"/>
			</wsdl:output>
		</wsdl:operation>
		<wsdl:operation name="GetDataUsingDataContract">
			<soap:operation soapAction="http://tempuri.org/IService/GetDataUsingDataContract" style="document"/>
			<wsdl:input>
				<soap:body use="literal"/>
			</wsdl:input>
			<wsdl:output>
				<soap:body use="literal"/>
			</wsdl:output>
		</wsdl:operation>
	</wsdl:binding>
	<wsdl:service name="Service">
		<wsdl:port name="BasicHttpBinding_IService" binding="tns:BasicHttpBinding_IService">
			<soap:address location="http://localhost:60817/Service.svc"/>
		</wsdl:port>
	</wsdl:service>
</wsdl:definitions>

What you want to be able to do to invoke a service endpoint directly is, of course, find its URL. In my case, with a little help from SoapUI, I figured out it was the same URL that started up initially:

http://localhost:60817/Service.svc

The first key is that you have to set up two headers to the request:

  • Content-Type
    • In my case, text/xml
  • SOAPAction
    • So for me, based on the WSDL above, that's http://tempuri.org/IService/GetData for the GetData action.

But you seem to have to send it a body in a POST -- GET gives you that stock opening page again. (Since GET and POST are REST conventions, I half-way expected the WCF service not to care, especially since we know GET can have a body now.)

I am not yet going to claim I know how the WSDL tells you what your input parameters should have looked like.

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tem="http://tempuri.org/">
   <soapenv:Header/>
   <soapenv:Body>
      <tem:GetData>
         <!--Optional:-->
         <tem:value>5</tem:value>
      </tem:GetData>
   </soapenv:Body>
</soapenv:Envelope>

Again, do this all in a Postman POST request with a body that's raw with XML as its type and you're golden. Change tem:value to another int to see more insanely interesting messages like...

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
    <s:Body>
        <GetDataResponse xmlns="http://tempuri.org/">
            <GetDataResult>You entered: 5</GetDataResult>
        </GetDataResponse>
    </s:Body>
</s:Envelope>

I do not miss XML.


Update: Ha! Wish I'd talked to myself from 13 years ago first!

Labels: , ,


posted by Jalindrine at 6/17/2025 07:27:00 PM
Friday, May 30, 2025

Note to self: It's pretty easy to get a Stripe CheckoutSession in .NET.

I talked about writing a Chrome extension and integrating payment through Stripe a few months ago. Unfortunately the "what appears to be an amazingly well-written howto" wasn't, quite.

So here's a quick bit of play with the Stripe API using .NET. This looks up information according to a "Checkout Session" id, which is what you'll get sent if you set up a "After Payment" processor URL, which is what I'm doing -- for now in an Azure Function, just to be kewl.

(No, you shouldn't use hard-coded paths in your code. No, you shouldn't pass around anonymous classed entities in C#. You're right, there's one instance of insanely not-DRY-ness in this code that almost bugs me. And I hate kludging ! after a nullable and would refactor that. This is pretty serious "proof of concept" territory here.)

Steps:

  1. Create a .NET console app.
    • For bonus points, use the newer template without a Main method version, I guess. I got this and was too lazy to start over.
  2. Add stripe.net to your project via your preferred NuGet manager.
  3. Get your Stripe secret key and put it in C:\temp\stripe.key
  4. Paste in the below junk, wiping out the Main method that's in there by default.
  5. Get a CheckoutSession id from, idk, say a Payment Link "After Payment" URL, and insert it into the code.
  6. Profit?
static void Main(string[] args)
{
    Console.WriteLine("Hello, World!");

    StripeConfiguration.ApiKey = File.ReadAllText(@"C:\temp\stripe.key").Trim();
    Service = new Stripe.Checkout.SessionService();

    var csId = "cs_test_a1RRCS1VWdOwOwvpJfdZCtjb9jSVOxp9ZhQibAI1Hv3dsvi6BfCPIWwdZP";
    var info = LookupStripeCheckoutSession(csId);

    Console.WriteLine(JsonConvert.SerializeObject(info, Formatting.Indented));
    File.WriteAllText(
        @"C:\temp\importantStuff.txt",
        JsonConvert.SerializeObject(info, Formatting.Indented)
    );
}

// Stripe Fees:
// https://stripe.com/pricing
// "Starts at 2.9% + 30ยข [per successful transaction for domestic cards]"
// Fudge.
public static object LookupStripeCheckoutSession(string checkoutSessionId)
{
    Session session = Service!.Get(checkoutSessionId);

    if (!session.Metadata.TryGetValue("appId", out var appId))
    {
        throw new MissingFieldException("Checkout session does not contain a product id");
    }

    System.IO.File.WriteAllText(
        @"C:\temp\session.txt",
        JsonConvert.SerializeObject(session, Formatting.Indented)
    );

    return new {
        CheckoutSessionId = session.Id,
        RufworkAppId = appId,
        session.AmountTotal,
        session.Created,

        session.PaymentStatus, // One of: <c>no_payment_required</c>, <c>paid</c>, or <c>unpaid</c>.

        // Note that TotalDetails (as with any child object brought over)
        // has its own json settings, so it'll, eg, serialize to snake case
        // if you don't munge it.
        session.TotalDetails,
        session.CustomerDetails.Email,
    };
}

I'd been thinking about having a $3/year [sic] subscription for an extension. After a test, I learned what the comments, above, mention: That'll cost me ($3 x .029 + 30ยข somehow equals...) 41ยข [???] per transaction.

Okay, apparently there's a "Usage fee" in addition to the charge?

Amount

Fees

Total

Type

Description

Created

Available on

-$0.02

โ€”

-$0.02

Stripe fee

Billing - Usage Fee (2025-05-26)

May 27

May 27

$3.00

-$0.39

$2.61

Charge

Subscription creation

May 26

Jun 2

From togai.com:

Stripe allows you to perform end-to-end financial transactions with its suite of integrated offerings:

Stripe Billing - This lets you create recurring subscriptions and invoices. Pricing starts at 2.9% + 30๏ฟ for every successful transaction.

Usage pricing for pay-as-you-go is 0.7% of billing volume.

If you need a Custom billing domain it is $10 per month.

Stripe Invoicing - Allows you to invoice upto 25 customers for free every month. Thereafter an overage of 0.4% is applied per invoice paid.

Stripe Tax - Offers you tools for tax calculation priced at 0.5% for each transaction.

Stripe Atlas - Assists you with setting up a company at a one-time setup fee of $500.

Stripe Sigma - Offers reporting and analytics using SQL. Pricing starts at 2๏ฟ per transaction and includes a $10 fee towards infrastructure. (emphasis mine -mfn)

From bossrevolution.com:

  • Billing.ย For businesses with a subscription-based or usage-based model, Stripe enables automated billing on a pay-as-you-go or monthly payment basis. Fees start at 0.7% of the billing volume or $620 per month for a one-year contract.

Cool cool. 13 2/3% of $3. Just below Apple's small dev 15%.

Good times. Least the API is simple. More to come.


From stripe.com:

Debit card purchases in the US are more extreme: there are no returns on interchange fees on any US debit card transaction.

If your business often processes refunds shortly after a payment, you can combat these potential lost fees by leaving the transaction authorization open rather than settling the sale right away. This is possible because you only pay interchange fees once a transaction is settled. If you leave the authorization open and a customer makes a return, you can simply reverse the authorization and avoid losing any extra interchange fees (since you never paid those fees to begin with).

For example, if you captured and settled a $100 debit card transaction and a customer requested a return, you could lose $0.42. However, if you had left the authorization open, you could only lose slightly less than $0.04.

You can generally leave an authorization open for up to two days before you pay additional fees, so this approach is most relevant for industries with the immediate delivery of goods (such as food delivery services).

What you can do:ย Configure the Stripe Payment Intents API toย separate authorization and capture.

good heavens.

Labels: , , , ,


posted by Jalindrine at 5/30/2025 01:26:00 PM
Wednesday, October 23, 2024

From stackoverflow.com:

Taken from MSDN's page on InvalidOperationException: "InvalidOperationException is used in cases when the failure to invoke a method is caused by reasons other than invalid arguments."ย 

โ€“ย STW
ย Commented Apr 21, 2009 at 19:44

I often forget what the "right" exception is to throw when it's not an argument issue -- and linters are getting better at reminding me not to be lazy and to stop using Exception with no subtype. "Code is evidence of the beliefs of its authors" after all.

I suppose InvalidOperationException is as good a fallback as any.

Labels: ,


posted by Jalindrine at 10/23/2024 10:31:00 AM
Friday, August 23, 2024

Okay, look, if there's one thing I'm tired of, it's half-baked example code that doesn't anticipate changes needed to push it into production.

Like the good ole WeatherForecastController from the .NET Core WebAPI template.

using Microsoft.AspNetCore.Mvc;

namespace MyApp .Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", 
            "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [HttpGet(Name = "GetWeatherForecast")]
        public IEnumerable<WeatherForecast> Get()
        {
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}

I mean, Visual Studio immediately complains:

Remove this unread private field '_logger' or refactor the code to use its value.

Well, duh. We have an endpoint with no logging. When would we need to log? Probably when we're doing something more complicated than creating random 8-ball style forecasts.

So let's pretend it's more difficult, throw in a try... catch, and actually log the exception.

[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
    try
    {
        return new ActionResult<IEnumerable<WeatherForecast>>(Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        }));
    }
    catch (Exception e)
    {
        _logger.LogError(e, "Something failed");
        return BadRequest("that didn't work");
    }
}

Guess what? Now we got TWO errors! YAY!!

  1. CS0029 Cannot implicitly convert type 'Microsoft.AspNetCore.Mvc.ActionResult<System.Collections.Generic.IEnumerable<MyApp.WeatherForecast>>' to 'System.Collections.Generic.IEnumerable<MyApp.WeatherForecast>'
  2. CS0266 Cannot implicitly convert type 'Microsoft.AspNetCore.Mvc.BadRequestObjectResult' to 'System.Collections.Generic.IEnumerable<MyApp.WeatherForecast>'. An explicit conversion exists (are you missing a cast?)

Dare you to tell me what to do next. Heck, I don't know. I do know WebAPIs have been around so long there are tons of wrong answers on the net.

Let's just show one example that does work and call it a day.

[HttpGet(Name = "GetWeatherForecast")]
public ActionResult<IEnumerable<WeatherForecast>> Get()
{
    try
    {
        return new ActionResult<IEnumerable<WeatherForecast>>(Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        }));
    }
    catch (Exception e)
    {
        _logger.LogError(e, "Something failed");
        return BadRequest("that didn't work");
    }
}

Why do I need to wrap the return type with ActionResult to be able to return BadRequest REST code? I don't know man. It should implicitly cast imo into any hard type return value. I mean it's not like we're literally returning IEnumerable<WeatherForecast> in the original code. We are doing some magic behind the scenes to make that a JSON return value for our consumers. Why not do the same for any REST code convenience type too?

Anyhow, I just want to remember this trick for the next time it happens so to the blog it goes.

:sigh:

Labels: , , ,


posted by Jalindrine at 8/23/2024 05:50:00 PM
Friday, February 19, 2021

One thing thatโ€™s more esoteric than it should be is how to create a good command-line application on macOS, or at least it is for the languages I know, which donโ€™t [really] include C.
 
I did briefly consider writing it in Swift, but it turns out C# is pretty easy but only if you know the right incantation. The key was finding the term `dotnet publish` (a link to which I found here). But then I needed a little more help to know the precise (or at least one precise) string of characters:

When you invoke the dotnet publish command it should end up being similar to the following:

$ dotnet publish -r osx.10.13-x64 /p:PublishSingleFile=true
Microsoft (R) Build Engine version 16.4.0+e901037fe for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

  Restore completed in 39.77 ms for /usr/local/share/dotnet/myApp/myApp.csproj.
  myApp -> /usr/local/share/dotnet/myApp/bin/Debug/netcoreapp3.1/osx.10.13-x64/myApp.dll
  myApp -> /usr/local/share/dotnet/myApp/bin/Debug/netcoreapp3.1/osx.10.13-x64/publish/

Thank heavens. Thanks again, random StackOverflow answerer.

In case you missed it, that means you type this:

dotnet publish -r osx.10.13-x64 /p:PublishSingleFile=true

...in the same directory as your sln file and then you call the exe that you created (almost certainly without the exe extension) from the publish folder:

/usr/local/share/dotnet/myApp/bin/Debug/netcoreapp3.1/osx.10.13-x64/publish/myApp

And now you've got a self-contained, macOS specific executable.

(I'm doing this to see how much trouble it'd be to write a macOS version of the Chutzpah JavaScript test runner on macOS. The VS Code extension is cross platform, almost by accident, but there's no equivalent for the "real" Windows version of Chutzpah exe that the extension, itself simply a pass-through runner, must call. Shouldn't be tough to recreate, though, at least for the "80%" use cases.)

Labels: , , ,


posted by Jalindrine at 2/19/2021 03:08:00 PM
Wednesday, February 14, 2018

I've been setting up a new greenfield project, and wanted to use not exactly a new stack, per se, so much as an "all-star" stack that'd capture the lessons I've learned coding recently. For me, that means it needs to be pretty straightforward, minimal dependencies, and minimal compilation. The fewer libraries and snowflake server requirements, the better.

On first take, after bypassing Node, which really is the winner,1 my guess was that my all-star stack would be...

  1. ASP.NET Core
    • C# with no OS limits.
    • Deploy inexpensively to Linode, eg, if you want.
  2. Web API on ASP.NET
    • No Razor templates (not that I hate Razor; just one less tech to maintain)
    • No direct interaction between server and views/html/client.
    • Keeps concerns separated perfectly, exchanging only JSON between tiers.
  3. Statically served html (with JavaScript packages, natch)
  4. jQuery 3.x slim
    • Slim means no ajax; it's more efficient to roll your own.
    • Every time I've used $.ajax, we've wrapped that with our own error handling, etc, anyhow.
    • Learn to spell XmlHttpRequest. ;^)
  5. Bootstrap
    • The cheapest mobile client is a responsive web client.
    • I realize native is better. But responsive is, give or take, free.
    • Also gives a decent basic theme.
  6. VueJS
    • This was probably the most difficult to decide on ahead of time
    • Will discuss more below.
  7. PostgreSQL or MySQL

VueJS was difficult to pick. I've used React, and almost solely React, for the last year. I like it. But I believe it contributes to monolithic source that's not necessarily factored well. The more interdependencies your code has, the more maintenance is an issue. And doing good React essentially requires, at this point, using Babel, and as soon as you're transpiling, well, the surrounding tooling and library dependencies skyrocket. Not to mention that debugging transpiled code either means you're wading into transpiler-generated muck, or you've got to maintain yet more tooling to step through things in something that's not the browser.

I was originally going to use Handlebars, which I've liked for years for its exceptional thinness & the way it encourages minimal business logic on the client, but caught Shawn Wildermuth on Jesse Liberty's Yet Another Podcast, and heard him talk about VueJS. VueJS is essentially Handlebars with, afaict, a few more recent lessons baked in. The syntax is actually very similar to Handlebars in many ways.

Here's what eventually won me over:

Unlike other monolithic frameworks, Vue is designed from the ground up to be incrementally adoptable. The core library is focused on the view layer only, and is easy to pick up and integrate with other libraries or existing projects. On the other hand, Vue is also perfectly capable of powering sophisticated Single-Page Applications when used in combination with modern tooling and supporting libraries.

It looks like we've got good room to grow using VueJS' conventions if we want to do a full SPA in the future.

I also debated not using Bootstrap, but I don't think it's worth the time to roll your own responsiveness when Bootstrap does it so well. It's over 200 KB total, but if you have it cached, that's a one-time hit that's smaller than most images. You're fine.

jQuery was also debatable, since you can handle DOM manipulation fairly well by wrapping vanilla JS with some convenience wrappers to check for DOM existence, but at 69 KB? Come on. It's well known, rock solid, and has good industry support. Use jQuery.

This looked nice. Simple, API-only and REST convention compliant server. No licensing fees. No JavaScript transpilation required, which will pay for itself in spades when you're debugging. Thin client. Clean separation of concerns.

I was going to go through my headaches setting everything up here, but I'll split that into a new post later. The quick version? Because of a wack nuget configuration I didn't realize was an issue (command line nugget didn't pick the "right" nugget server; I had a local one set up in VS2017 as well), I'm using SQL Server for now, though I will probably give PostgreSQL another look before I settle on a dbms.


1 If you want the cleanest stack, there's nothing more advantageous than using the same language on the server and the client. Node lets you do that. There's some overhead (learning & setup tedium) standing up a web server -- do you just use Express everywhere, or serve from Nginx? -- but nodejs is The Right Answer, imo. But I'm a C# guy "professionally", and there still seem to be more opening for C# devs than node, so I think there's an argument that a C# server-side should be easier for a customer to have maintained going forward.

Labels: , , , ,


posted by ruffin at 2/14/2018 11:10:00 AM
Thursday, October 20, 2016

From microsoft.com (via StackOverflow):

In order to be able to be instantiated as an object element, your class must meet the following requirements:

  • Your custom class must be public and support a default (parameterless) public constructor. (See following section for notes regarding structures.)

  • Your custom class must not be a nested class. Nested classes and the "dot" in their general CLR usage syntax interfere with other WPFย and/or XAML features such as attached properties.

[emphasis is mine]

That's got to be a joke, right? So I can't use a "nested class" (which would include anything with a java-style like com.yourCo.namespace) like this to create a ListView item template?

<ListView Grid.Row="1" Grid.RowSpan="1"
            Name="lstEps"
            IsItemClickEnabled="True">
    <ListView.ItemTemplate>


        <DataTemplate x:DataType="PodcastersFeed.Entities.EpCapsule"><!-- <<< No can haz namespace!! -->


            <Grid>
                <Border Background="LightGray" Height="30" Width="250">
                    <TextBlock
                        Text="{x:Bind Title}"
                        FontSize="18"
                        Foreground="Green"/>
                </Border>
            </Grid>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

Look, that's crazy-insane. I'd also had trouble with "namespaced" UserControls I'd created in the past, and lucked into figuring out they'd only work for me when I took them "out" of the non-local namespace, but figured I was doing something wrong. Maybe not.

Wow. I've made a wrapper class in my local namespace as a shuttle.

UWP unfortunately remains a hacky place.

Labels: , ,


posted by ruffin at 10/20/2016 04:03:00 PM
Friday, October 14, 2016

Let's talk HTML Fragments in your clipboard when coding on Windows. I've created what's arguably the best Markdown editor on Win10 (if I do say so myself), and wanted it to be more intelligent when I was pasting content that comes from web browsers. That is, in the original version of MarkUpDown, if you copied the text I've got highlighted, below, from DaringFireball...

Quote from Walt Mossberg on Siri plus a little bit from Gruber, now with visible context menu saying "Copy"

... and you pasted it into MarkUpDown as a quotation (Ctrl-Shift-V), you'd get unstyled text, like this...

> Mossberg:
> For instance, when I asked Siri on my Mac how long it would take me to get to work, it said it didnโ€™t have my work address โ€” even though the โ€œmeโ€ contact card contains a work address and the same synced contact card on my iPhone allowed Siri to give me an answer.
> Similarly, on my iPad, when I asked what my next appointment was, it said โ€œSorry, Walt, somethingโ€™s wrongโ€ โ€” repeatedly, with slightly different wording, in multiple places on multiple days. But, using the same Apple calendar and data, Siri answered correctly on the iPhone.
> These sort of glaring inconsistencies are almost as bad as universal failures. The big problem Apple faces with Siri is that when people encounter these problems, they stop trying.

I mean, that's okay-ish. Unfortunately, you'd have to go put in an additional line after each line so that it's not all scrunched into one paragraph. And you lose that Gruber was block-quoting Mossberg. And you lose that "they stop trying" was originally in italics. That stinks.

Isn't Markdown really supposed to be shorthand for html? Can't we handle pasting html better than that?

Yes, yes we can...


TL;DR

Go pull the HtmlFragmentHelper, a working, in-progress library for turning HTML Fragment strings into view models with these properties...

public class HtmlFragmentViewModel
{
    public string Version = "";
    public int StartHtml = int.MinValue;
    public int EndHtml = int.MinValue;
    public int StartFragment = int.MinValue;
    public int EndFragment = int.MinValue;
    public string SourceUrl = "";
    public string FragmentSourceRaw = "";
    public string Error = "";

    //...
}

There are also several convenience methods to help you get, say, the top- and second-level domains for the HTML Fragment's source URL (example.com from http://blog.example.com/user/blog0000231.html), or just the html from the selected fragment (myHtmlFragmentViewModel.FragmentSourceParsed), etc.

Here's an example:

public string CreateQuote(string htmlFragmentSource) 
{
    string strIntroLink = string.Empty;
    HtmlFragmentViewModel vm = new HtmlFragmentViewModel(htmlFragmentSource);

    if (vm.SourceUrl.Length > 0 && vm.SourceUrlDomainSecondAndTopLevelsOnly.Length > 0)
    {
        strIntroLink = string.Format("From <a href=\"{0}\">{1}</a>:" + Environment.NewLine + Environment.NewLine,
            vm.SourceUrl, vm.SourceUrlDomainSecondAndTopLevelsOnly);
    }

    return strIntroLink + vm.FragmentSourceParsed;
}

Read how to use it here. Get it from GitHub, though the only file you really need is here.

And here's a quick article on how to get code back into the clipboard as html, which I'm not doing.


HTML Fragment Format

Want to paste clipboard HTML as HTML? Enter Microsoft's HTML clipboard format. Instead of using the standard code to read the clipboard's contents, like this...

string clipboardText = await Clipboard.GetContent().GetTextAsync();

... now you use this...

string htmlClipboardText = await Clipboard.GetContent().GetHtmlFormatAsync();

But when you look at the contents of htmlClipboardText, you quickly notice that strange things are afoot at the Circle K.

What number are you thinking of?

What comes back isn't a string with html source, but an "HTML Fragment" formatted string that looks like this:

HTML Fragment from Edge

Version:1.0
StartHTML:000000210
EndHTML:000003550
StartFragment:000002696
EndFragment:000003500
StartSelection:000002696
EndSelection:000003496
SourceURL:http://daringfireball.net/2016/10/mossberg_siri
<!DOCTYPE HTML>
<HTML lang="en"><HEAD>       <!-- Open Graph [jive] --> <!-- 
    <meta property="og:site_name"   content="Daring Fireball" />
    <meta property="og:title"       content="Walt Mossberg: โ€˜Why Does Siri Seem So Dumb?โ€™" />
    <meta property="og:url"         content="http://daringfireball.net/2016/10/mossberg_siri" />
    <meta property="og:description" content="In addition to the engineering hurdles to actually make Siri much better, Apple also has to overcome a โ€œboy who cried wolfโ€ credibility problem." />
    <meta property="og:image"       content="https://daringfireball.net/graphics/df-square-192" />
    <meta property="og:type"        content="article" />
 -->         <!-- Twitter Card [jive] -->                    <TITLE>Daring Fireball: Walt Mossberg: โ€˜Why Does Siri Seem So Dumb?โ€™</TITLE>        <LINK href="/graphics/apple-touch-icon.png" rel="apple-touch-icon-precomposed">     <LINK href="/graphics/favicon.ico?v=005" rel="shortcut icon">   <LINK href="/graphics/dfstar.svg" rel="mask-icon" color="#4a525a">  <LINK href="/css/fireball_screen.css?v1.7" rel="stylesheet" type="text/css" media="screen">     <LINK href="/css/ie_sucks.php" rel="stylesheet" type="text/css" media="screen">     <LINK href="/css/fireball_print.css?v01" rel="stylesheet" type="text/css" media="print">    <LINK href="/feeds/main" rel="alternate" type="application/atom+xml">   
<SCRIPT src="/mint/?js" type="text/javascript" async=""></SCRIPT>

<SCRIPT src="http://www.google-analytics.com/ga.js" type="text/javascript" async=""></SCRIPT>

<SCRIPT src="/js/js-global/FancyZoom.js" type="text/javascript"></SCRIPT>

<SCRIPT src="/js/js-global/FancyZoomHTML.js" type="text/javascript"></SCRIPT>
     <LINK title="Home" href="/" rel="home">     <LINK href="http://df4.us/pfz" rel="shorturl">  <LINK title="Apple Responds to Dash Controversy" href="http://daringfireball.net/2016/10/apple_dash_controversy" rel="prev">    
<SCRIPT src="http://daringfireball.net/mint/?record&amp;key=383950464d37374b39333637695970466458724e6779513431&amp;referer=&amp;resource=http%3A//daringfireball.net/2016/10/mossberg_siri&amp;resource_title=Daring%20Fireball%3A%20Walt%20Mossberg%3A%20%u2018Why%20Does%20Siri%20Seem%20So%20Dumb%3F%u2019&amp;resource_title_encoded=0&amp;window_width=1756&amp;window_height=921&amp;resolution=2438x1371&amp;flash_version=0&amp;1476397798179&amp;serve_js" type="text/javascript"></SCRIPT>
</HEAD><BODY onload="setupZoom()"><DIV id="Box"><DIV id="Main"><DIV class="article"><!--StartFragment--><P>Mossberg:</P><BLOCKQUOTE><P>For instance, when I asked Siri on my Mac how long it would take me to get to work, it said it didnโ€™t have my work address โ€” even though the โ€œmeโ€ contact card contains a work address and the same synced contact card on my iPhone allowed Siri to give me an answer.</P><P>Similarly, on my iPad, when I asked what my next appointment was, it said โ€œSorry, Walt, somethingโ€™s wrongโ€ โ€” repeatedly, with slightly different wording, in multiple places on multiple days. But, using the same Apple calendar and data, Siri answered correctly on the iPhone.</P></BLOCKQUOTE><P>These sort of glaring inconsistencies are almost as bad as universal failures. The big problem Apple faces with Siri is that when people encounter these problems, <EM>they stop trying</EM>.</P><!--EndFragment--></DIV></DIV></DIV></BODY></HTML>

Wow. No, really, wow. That surprised me. I was using Edge this time instead of Chrome, and whoa. Edge includes the entire page's header. That said, that's really not a horrible idea. We have really good context for this fragment, and if we wanted the original CSS, for example, we know where to get it.

Aside: Look at the info DaringFireball's sucking in:

resolution=2560x1440&amp;
flash_version=0&amp;
1476452395012&amp;
serve_js

Gruber doesn't use Flash. Kinda Panopticlicky, ain't it? Looks like it's supporting this.

Another Aside: Internet Explorer 11 creates the same fragment source, so if you think Edge was created from the ground up...

Interestingly, the old Internet Explorer copyright box used to reference Mosaic, but that's gone in IE11. Wonder if IE11 is really the child of IE6 and 7? But enough aside-ing...

HTML Fragment from Chrome

But let's take a look at where I initially started, with Chrome.

Version:0.9
StartHTML:0000000164
EndHTML:0000002719
StartFragment:0000000200
EndFragment:0000002683
SourceURL:http://daringfireball.net/2016/10/mossberg_siri
<html>
<body>
<!--StartFragment--><p style="margin: 0px 0px 1.6em; padding: 0px; color: rgb(238, 238, 238); font-family: Verdana, &quot;Bitstream Vera Sans&quot;, sans-serif; font-size: 11px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(74, 82, 90);">Mossberg:</p><blockquote style="font-size: 11px; margin: 2em 2em 2em 1em; padding: 0px 0.75em 0px 1.25em; border-left: 1px solid rgb(119, 119, 119); border-right: 0px solid rgb(119, 119, 119); outline: 0px; vertical-align: baseline; background: rgb(74, 82, 90); color: rgb(238, 238, 238); font-family: Verdana, &quot;Bitstream Vera Sans&quot;, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-t
ransform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;"><p style="margin: 0px 0px 1.6em; padding: 0px;">For instance, when I asked Siri on my Mac how long it would take me to get to work, it said it didnโ€™t have my work address โ€” even though the โ€œmeโ€ contact card contains a work address and the same synced contact card on my iPhone allowed Siri to give me an answer.</p><p style="margin: 0px 0px 1.6em; padding: 0px;">Similarly, on my iPad, when I asked what my next appointment was, it said โ€œSorry, Walt, somethingโ€™s wrongโ€ โ€” repeatedly, with slightly different wording, in multiple places on multiple days. But, using the same Apple calendar and data, Siri answered correctly on the iPhone.</p></blockquote><p style="margin: 0px 0px 1.6em; padding: 0px; color: rgb(238, 238, 238); font-family: Verdana, &quot;Bitstream Vera Sans&quot;, sans-serif; font-size: 11px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacin
g: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(74, 82, 90);">These sort of glaring inconsistencies are almost as bad as universal failures. The big problem Apple faces with Siri is that when people encounter these problems,<span class="Apple-converted-space">ย </span><em>they stop trying</em>.</p><!--EndFragment-->
</body>
</html>

See the difference? Chrome's fragment initially looks much more focused, but also provides much less information about the code's original context. Worse, Chrome's puts all the CSS information inline, and repeating each CSS property inside of each pasted html element makes the source a long ways away from DRY. And that out-of-context, inline CSS is lossily translated, which will cause us some rendering problems that we'll see and discuss in a bit.

Original Source

Now Edge's fragment isn't perfect. Here's the original html of that snippet straight from the server...

<p>Mossberg:</p>

<blockquote>
  <p>For instance, when I asked Siri on my Mac how long it would take
me to get to work, it said it didnโ€™t have my work address &#8212;
even though the โ€œmeโ€ contact card contains a work address and
the same synced contact card on my iPhone allowed Siri to give
me an answer.</p>

<p>Similarly, on my iPad, when I asked what my next appointment was,
it said โ€œSorry, Walt, somethingโ€™s wrongโ€ &#8212; repeatedly, with
slightly different wording, in multiple places on multiple days.
But, using the same Apple calendar and data, Siri answered
correctly on the iPhone.</p>
</blockquote>

<p>These sort of glaring inconsistencies are almost as bad as universal failures. The big problem Apple faces with Siri is that when people encounter these problems, <em>they stop trying</em>.

NOTE: There's no closing </p> tag because I've cut the source at that spot.

Look how clean the original is! Why can't we just get that? Chrome's CSS injections and line flattening makes for a much less human readable snippet.

Edge is close, but still weird. Take another look. For some reason, Edge's fragment makes every tag uppercase like it's 1998. It also trashes Gruber's original (and thoughtful) whitespace. Also yuck. I mean, Gruber's using Markdown to write Daring Fireball. In a perfect world, I'd take that clipboard's html and turn it back into Markdown when it's pasted into MarkUpDown.

How do we get this original source?

Start and End Fragment

The important thing to catch is that the HTML Fragment format almost always includes a full html doc with html and body tags after the SourceURL and other metadata. Within that html code, there are markers, <!--StartFragment--> and <!--EndFragment-->, saying where the exact selection started and stopped.

Though Chrome immediately dives into your selected text (the "true" html fragment) immediately after the <body> tag, Edge gives you any open tags that come before your fragment too. In this case, you had three open div tags when you got to the highlighted selection: <DIV id="Box"><DIV id="Main"><DIV class="article">. Knowing this DOM could be really helpful if you were interested in retrieving the original CSS and formatting your fragment as close to the original markup as possible.

If you ignore context like this, though, it's an easy enough feat to take the string and parse out the "true fragment's" html by splitting the entire HTML Fragment string on <!--StartFragment--> and <!--EndFragment-->. Here's an early version of something I wrote to just take out the html, ignoring all the fragment metadata.

private string _parseHtmlClipboardFragment(string rawFragmentSource)
{
    string ret = rawFragmentSource;
    string delimiterStartAfter = "<!--StartFragment-->";
    string delimiterEndBefore = "<!--EndFragment-->";

    if (-1 < ret.IndexOf(delimiterStartAfter))
    {
        ret = ret.Substring(ret.IndexOf(delimiterStartAfter) + delimiterStartAfter.Length);
        if (-1 < ret.IndexOf(delimiterEndBefore))
        {
            ret = ret.Substring(0, ret.IndexOf(delimiterEndBefore));
        }
        else
        {
            ret = string.Empty;  // No luck, Ending not after Start; go back to nothing.
        }
    }

    return ret;
}

NOTE: I've got a nice library that parses all this up into a custom html fragment view model that I mentioned in TL;DR. I wouldn't and don't use the above code in most cases.

When we parse out that html and insert it into our Markdown from Chrome, however, we get some, um, interesting results.

NOTE: Remember that Markdown is a superset, so to speak, of html. You could have a "Markdown" file without any Markdown syntax that's pure html. So what I'm really saying is, "Let's see what those html snippets that we pulled from the fragment look like when they're injected into our regularly scheduled html."

I'm going to wrap these pastes in blockquote tags so that you can tell where they start and stop easily.

Edge Parsed Code

Mossberg:

For instance, when I asked Siri on my Mac how long it would take me to get to work, it said it didnโ€™t have my work address โ€” even though the โ€œmeโ€ contact card contains a work address and the same synced contact card on my iPhone allowed Siri to give me an answer.

Similarly, on my iPad, when I asked what my next appointment was, it said โ€œSorry, Walt, somethingโ€™s wrongโ€ โ€” repeatedly, with slightly different wording, in multiple places on multiple days. But, using the same Apple calendar and data, Siri answered correctly on the iPhone.

These sort of glaring inconsistencies are almost as bad as universal failures. The big problem Apple faces with Siri is that when people encounter these problems, they stop trying.

Though you should remember that the original source has been smeared together, losing its newlines, and all the tags are strangely capitalized, the source looks pretty nice. It's plain, and adopts the local styling for the most part, but the most important markup is still there -- the blockquote, the italics, links if they'd been included, etc.

Chrome Parsed Code

Mossberg:

For instance, when I asked Siri on my Mac how long it would take me to get to work, it said it didnโ€™t have my work address โ€” even though the โ€œmeโ€ contact card contains a work address and the same synced contact card on my iPhone allowed Siri to give me an answer.

Similarly, on my iPad, when I asked what my next appointment was, it said โ€œSorry, Walt, somethingโ€™s wrongโ€ โ€” repeatedly, with slightly different wording, in multiple places on multiple days. But, using the same Apple calendar and data, Siri answered correctly on the iPhone.

These sort of glaring inconsistencies are almost as bad as universal failures. The big problem Apple faces with Siri is that when people encounter these problems,ย they stop trying.

Um, ew. Wha' happen'd?

Unfortunately, even though Daring Fireball's entire page has the same styles applied, this inline kludge Chrome performs puts it only in each, in this case, <p> tag. So when we have a margins around paragraphs, there's nothing telling us to also put that color and style between the block-level tags. There's no surrounding tag to be a stand-in for, essentially, the body (or any other lost, open) tag.

Let's look at all the overhead we have for each p from that fragment:

<p style="
    margin: 0px 0px 1.6em;
    padding: 0px;
    color: rgb(238, 238, 238);
    font-family: Verdana, &quot;Bitstream Vera Sans&quot;, sans-serif;
    font-size: 11px;
    font-style: normal;
    font-variant-ligatures: normal;
    font-variant-caps: normal;
    font-weight: normal;
    letter-spacing: normal;
    orphans: 2;
    text-align: left;
    text-indent: 0px;
    text-transform: none;
    white-space: normal;
    widows: 2;
    word-spacing: 0px;
    -webkit-text-stroke-width: 0px;
    background-color: rgb(74, 82, 90);
">

And here's what we have from Chrome's Dev Tools' "Computed" style tab:

color: rgb(238, 238, 238);
display: block;
font-family: Verdana, "Bitstream Vera Sans", sans-serif;
font-size: 11px;
height: 19px;
line-height: 19.8px;
margin-bottom: 17.6px;
margin-left: 0px;
margin-right: 0px;
margin-top: 0px;
padding-bottom: 0px;
padding-left: 0px;
padding-right: 0px;
padding-top: 0px;
text-align: left;
text-size-adjust: 100%;
width: 425px;
-webkit-margin-after: 17.6px;
-webkit-margin-before: 0px;
-webkit-margin-end: 0px;
-webkit-margin-start: 0px;

But note that that's essentially all from one css file, as properties from user agent stylesheets are simply the browser's defaults:

Paragraph styles for Daring Fireball

If you'd like, we can compare the fragment's CSS vs. the stylesheet. Spoiler: It's a mess. Not a lot of matching. Click here to see. Ugly.

The bottom line is that background-color: rgb(74, 82, 90); is something that belongs to the body, but is inserted here by Chrome into its HTML Clipboard contents as something that's attached to each block element. That's wrong. It doesn't display correctly, and it needlessly clutters the resulting HTML.

With HtmlFragmentHelper, I have a couple of choices. One is to strip all inline styles out, or at least some subset of styling. If I want to maintain as much of the original as possible, I might just blast anything that injects colors into the html, like color and background-color. Or I could blast all the inline style info and get something more like Edge's code.

Or I could normalize the CSS and optionally (via a property on HtmlFragmentViewModel) wrap it all in a block-level tag that contains CSS that's everywhere. Or at least in all of the top-level tags. But now I'm starting to write an html parser, which is a little outside of my current scope.

I think I'm going to do the former, though it's not in there as of this writing. That Chrome paste looks horrible as is, though. If only there was an easier way...


HTML Fragment from Firefox

There are certainly other applications that create HTML Clipboard values. I ran into LibreOffice's when using Calc, their spreadsheet app, and its html clipboard doesn't include Start and EndFragment tags. Wonderful, folks, wonderful.

But isn't there one glaring omission I might want to add here? You know, I think there is at least one...

For fun, let's take a look at Firefox's fragment too. Firefox isn't as popular as it used to be, but it's probably worth taking a look. (Remember that this is all on Windows, and Safari is dead enough by now that I'm not going to bother with v5.)

Version:0.9
StartHTML:00000156
EndHTML:00001060
StartFragment:00000190
EndFragment:00001024
SourceURL:http://daringfireball.net/2016/10/mossberg_siri
<html><body>
<!--StartFragment--><p>Mossberg:</p>

<blockquote>
  <p>For instance, when I asked Siri on my Mac how long it would take
me to get to work, it said it didnโ€™t have my work address โ€”
even though the โ€œmeโ€ contact card contains a work address and
the same synced contact card on my iPhone allowed Siri to give
me an answer.</p>

<p>Similarly, on my iPad, when I asked what my next appointment was,
it said โ€œSorry, Walt, somethingโ€™s wrongโ€ โ€” repeatedly, with
slightly different wording, in multiple places on multiple days.
But, using the same Apple calendar and data, Siri answered
correctly on the iPhone.</p>
</blockquote>

<p>These sort of glaring inconsistencies are almost as bad as universal 
failures. The big problem Apple faces with Siri is that when people 
encounter these problems, <em>they stop trying</em>.</p><!--EndFragment-->
</body>
</html>

Oh, so beautiful. Same whitespace as the original, with the added bonus (?) that the final paragraph is also text wrapped. No crazy inline CSS attempted. No header overhead or 1998-style HTML TAGS.

You could argue that Chrome is better because it has that inline CSS (yuck!) or Edge because it has the full page headers, but honestly, we're only one step farther away from what Edge gave us. We have the URL here. If we have network access to get CSS, we have network access to read the header and figure out where the CSS lives ourselves. Don't overcomplicate things. Beautiful. Good looking code continues to look good when pasted inline with Markdown.

Firefox Parsed Code

And Firefox's snippet is just as nice when pasted.

Mossberg:

For instance, when I asked Siri on my Mac how long it would take me to get to work, it said it didnโ€™t have my work address โ€” even though the โ€œmeโ€ contact card contains a work address and the same synced contact card on my iPhone allowed Siri to give me an answer.

Similarly, on my iPad, when I asked what my next appointment was, it said โ€œSorry, Walt, somethingโ€™s wrongโ€ โ€” repeatedly, with slightly different wording, in multiple places on multiple days. But, using the same Apple calendar and data, Siri answered correctly on the iPhone.

These sort of glaring inconsistencies are almost as bad as universal failures. The big problem Apple faces with Siri is that when people encounter these problems, they stop trying.

I miss Firefox.

Labels: , , , , , , ,


posted by ruffin at 10/14/2016 11:46:00 AM
Friday, September 09, 2016

Oh, Pivots in UWP, you so crazy.

You may have heard that I'm writing a Markdown editor, MarkUpDown. It can have several files open at once, of course, and I'm using a UWP Pivot control to handle navigation.

As I got closer to releasing, it hit me that Ctrl-W, though common, isn't the most discoverable way to close those PivotItems. And now that I'm maintaining state between starts, so that whatever tabs you have open when you close the app are back on restart, closing each file "on purpose" is a lot more important than when they all closed on each restart.

That is, it looks like I need a UI for closeable tabs.

Not as easily said as done. You can customize the Header of the PivotItem pretty easily. In fact, a Header can be any object. You create a custom UserControl, throw in your favorite UI widgets, and you're golden.

In that UserControl, I put a TextArea and an icon of some sort, here a box with an X in it donated from Material Design Icons... Took a little experimenting to figure out exactly how to put text and an icon into the header presentably, though. I initially used a ViewBox, which seemed to scale the Path style image I'm using fairly well, no resizing necessary, but the ViewBox hated responding to clicks (or, in this case, "Taps" and "Presses"). Apparently you can manually set something called HandledEventsToo to make it catch events that are already caught by something with higher priority, but that strange Boolean reminds me of CSS' !important a little too much.

I replaced the ViewBox with a Button to be conventional, and then placed the Path style icon in there. I've got decent click event handling now, though it was difficult (well, at least "non-trivial") to get the icon to display centered. I went through the old process of using WPF Geometry Transformer to "Tansform" [sic] the Path into something smaller, and poof... -ish.

<PivotItem
    x:Class="CloseableTabs.CloseableTabHeader"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:CloseableTabs"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400">

    <StackPanel Orientation="Horizontal">
        <TextBlock Margin="0,0,0,0" Name="txtHeader" FontSize="18">Test</TextBlock>
        <Button Background="White" Click="Button_Click">
            <Button.Content>
                <PathIcon Data="M13.3000001907349,2.09999990463257L3.5,2.09999990463257 2.51005053520203,2.51005053520203 2.09999990463257,3.5 2.09999990463257,13.3000001907349 2.51005053520203,14.2899494171143 3.5,14.6999998092651 13.3000001907349,14.6999998092651 14.2899494171143,14.2899494171143 14.6999998092651,13.3000001907349 14.6999998092651,3.5 14.2899494171143,2.51005053520203 13.3000001907349,2.09999990463257 M13.3000001907349,13.3000001907349L3.5,13.3000001907349 3.5,3.5 13.3000001907349,3.5 13.3000001907349,13.3000001907349 M11.8999996185303,5.87999963760376L9.3799991607666,8.39999961853027 11.8999996185303,10.9200000762939 10.9200000762939,11.8999996185303 8.39999961853027,9.3799991607666 5.87999963760376,11.8999996185303 4.90000009536743,10.9200000762939 7.42000007629395,8.39999961853027 4.90000009536743,5.87999963760376 5.87999963760376,4.90000009536743 8.39999961853027,7.42000007629395 10.9200000762939,4.90000009536743 11.8999996185303,5.87999963760376z" />
            </Button.Content>
        </Button>
    </StackPanel>
</PivotItem>

That's fun, isn't it?

example of two closeable tabs

That looks okay, but there are still two problems.

  1. How do I set the Label (now placed in txtHeader) of my UserControl in XAML?
  2. How I do I remove the PivotItem once its button is clicked.

Okay, three problems -- I really should change the icon to a greyed-out version when the PivotItem's not active. That looks awful. Mark that down for version two.

Accessing a property in custom XAML from C

I found the answer to the first pretty easily: You have to use a Dependency Property, of course!

Dependency property values are not stored as fields on the class, they are stored by the xaml framework, and are referenced using a key, which is retrieved when the property is registered with the Windows Runtime property system by calling the DependencyProperty.Register method.

But wait! There's more!

Defining a dependency property can be thought of as a set of concepts. These concepts are not necessarily procedural steps, because several concepts can be addressed in a single line of code in the implementation. This list gives just a quick overview. We'll explain each concept in more detail later in this topic, and we'll show you example code in several languages.

  • Register the property name with the property system (call Register), specifying an owner type and the type of the property value.
  • There's a required parameter for Register that expects property metadata. Specify null for this, or if you want property-changed behavior, or a metadata-based default value that can be restored by calling ClearValue, specify an instance of PropertyMetadata.
  • Define a DependencyProperty identifier as a public static readonly property member on the owner type.
  • Define a wrapper property, following the property accessor model that's used in the language you are implementing. The wrapper property name should match the name string that you used in Register. Implement the get and set accessors to connect the wrapper with the dependency property that it wraps, by calling GetValue and SetValue and passing your own property's identifier as a parameter.
  • (Optional) Place attributes such as ContentPropertyAttribute on the wrapper.

Note If you are defining a custom attached property, you generally omit the wrapper. Instead, you write a different style of accessor that a XAML processor can use. See Custom attached properties.

... [Oh, but there's more... -mfn] ...

In some scenarios, you are defining dependency properties for objects that are used on more than one UI thread. This might be the case if you are defining a data object that is used by multiple apps, or a control that you use in more than one app. You can enable the exchange of the object between different UI threads by providing a CreateDefaultValueCallback implementation rather than a default value instance, which is tied to the thread that registered the property.

Tis a silly place

I feel like the Knights of the Round Table after they've considered whether to go to Camelot. Okay, nevermind. 'Tis a silly place. I won't make these new headers XAML-friendly for "version mvp". We're going to set the Label somewhere outside of XAML. Moving on to #2...


Removing a PivotItem on custom UI click

My first bright idea, which I've used before, is to take the sender, cast to the appropriate widget, find its Parent, and just slowly climb my way back up the UI hierarchy until I find what I need. That is, I'll drill up, like a jQuery closest, until I have the PivotItem, then the Pivot, that I need. Then I can remove the PivotItem from the Pivot's Items collection. Easy, right? The best part is that this normally doesn't change, and you can have the PivotItem remove itself without any serious hooking in with what's around it. (I could probably do this more impressively with reflection, and essentially hook an extension method up for UIElement that parallels the jQuery closest method I references earlier.)

But crimminy. Here's the drill-up code I ended up with, originally in the UserControl's Button's Click event. Remember that this is just exploratory code at this point.

System.Diagnostics.Debug.WriteLine("Click");
Button btn = (Button)sender;
System.Diagnostics.Debug.WriteLine(btn.Parent.GetType());

var spam = (StackPanel)(btn.Parent);

var header = (CloseableTabHeader)(spam.Parent);
var headerItem = (PivotHeaderItem)(header.Parent);

PivotHeaderPanel headerPanel = (PivotHeaderPanel)(headerItem.Parent);
Grid weirdPiGrid = (Grid)headerPanel.Parent;

ContentControl contentControl = (ContentControl)weirdPiGrid.Parent;
Grid anotherGrid = (Grid)contentControl.Parent;
PivotPanel pivotPanel = (PivotPanel)anotherGrid.Parent;
ScrollViewer scrollViewer = (ScrollViewer)pivotPanel.Parent;
Grid yaGrid = (Grid)scrollViewer.Parent;
Grid yaGrid2 = (Grid)yaGrid.Parent;

That's interesting, right? Not only is it much more convoluted a design than I would've expected, we also never get a PivotItem or a Pivot -- yaGrid2 is the "root element" of the page, I think. And the stuff we do get, like PivotPanel, come with warnings like this:

PivotPanel isn't typically used for general XAML UI composition.

/sigh This was easy, wasn't it? My UI traversal has me lost in the funhouse rather than in a cleanly executed UI widget hierarchy that follows its own mental model. Feels like I went out of the back door and I'm seeing the outside of the house isn't finished quite yet. Unfortunately, I get the impression that UWP is held together by chicken wire and duct tape a lot more than I'm used to seeing in a Microsoft UI toolkit.

There's probably some easy way to save what I'm trying to do from the fire, but forget it. For version mvp, I'm changing the CloseableHeader constructor from this...

public CloseableTabHeader(string headerText) : base()
{
    this.InitializeComponent();
    txtHeader.Text = headerText;
}

... to this...

public PivotItem itemParent = null;
public Pivot pivotParent = null;

public CloseableTabHeader(string headerText, PivotItem itemParent, Pivot pivotParent) : base()
{
    this.InitializeComponent();
    txtHeader.Text = headerText;
    this.itemParent = itemParent;
    this.pivotParent = pivotParent;
}

I think you see where I'm going. Yes, I feel a little dirty too. Now I can remove it easily in the Button's Click event:

private void Button_Click(object sender, RoutedEventArgs e)
{
    if (null != this.itemParent && null != this.pivotParent)
    {
        this.pivotParent.Items.Remove(this.itemParent);
    }
}

Hrm. Perhaps "parentPivot" is better than "pivotParent", and same with itemParent. In any event, you want to bubble up to the closest PivotItem and Pivot, and this makes that stupidly straightforward, though I do wonder how much Dispose-ing I should be doing.

I don't love that set-up, and I really dislike the way the buttons' bounds "highlight" simply when you mouse over the icon, like this:

highlighted close button

... but overall, that's a workable solution that will make discovery for tab closure much more straightforward for non-keyboard addicts.

Labels: , , ,


posted by ruffin at 9/09/2016 08:56:00 AM
Friday, August 12, 2016

When you write pretty much any code dealing with text, you get used to viewing raw bytes. Probably the most annoying part of it all, once you get your head around Unicode, is dealing with newlines in a tolerant, cross-platform fashion.

From today's Wikipedia:

0x9B? Good heavens. And if "OverDose On Arrival" isn't stuck in your head somewhere, you haven't really parsed text yet in your programming career. To these, you might also add 0D0D0A from an apparent Notepad bug.

The most annoying part is probably that you can't easily treat newline characters as any fixed number of bytes when you're dealing with crossplatform text. And you should probably always assume you're dealing with crossplatform text.


So when I'm breaking things down into informal test cases, I often shove the parsing code I'm writing into a testing console app project to see what's going on. Why I never did the below before when dealing with newlines, I have no idea.

public static void NLWriteLine(string toWrite, bool bookend = true)
{
    if (bookend)
    {
        toWrite = "#" + toWrite + "#";
    }

    Console.WriteLine(toWrite.Replace("\r", "\\r\r").Replace("\n", "\n\\n") + "\n\n");
}

If I ignore Acorn BBC and RISC OS (...and done), every \n or LF either comes after other NL bytes or by itself. Put in the text version of it afterwards, duh. Vice versa the \r.

So much nicer to have...

#
\nstring with some\r
\nnewlines#

... rather than trying to figure out if a cut caught the CR previous to that \n by printing int values or something similarly overcomplicated.

Labels: , , ,


posted by ruffin at 8/12/2016 01:05:00 PM

<< Older | Newer >>


Support freedom
All posts can be accessed here:


Just the last year o' posts:

URLs I want to remember:
* Atari 2600 programming on your Mac
* joel on software (tip pt)
* Professional links: resume, github, paltry StackOverflow * Regular Expression Introduction (copy)
* The hex editor whose name I forget
* JSONLint to pretty-ify JSON
* Using CommonDialog in VB 6 * Free zip utils
* git repo mapped drive setup * Regex Tester
* Read the bits about the zone * Find column in sql server db by name
* Giant ASCII Textifier in Stick Figures (in Ivrit) * Quick intro to Javascript
* Don't [over-]sweat "micro-optimization" * Parsing str's in VB6
* .ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture); (src) * Break on a Lenovo T430: Fn+Alt+B
email if ya gotta, RSS if ya wanna RSS, (?_?), ยข, & ? if you're keypadless


Powered by Blogger etree.org Curmudgeon Gamer badge
The postings on this site are [usually] my own and do not necessarily reflect the views of any employer, past or present, or other entity.