I was glad that the programs had been doing their job - but I was disappointed that I couldn't verify they had done their job more quickly. The data about runs needed a better presentation - enter LiveCharts2 (on GitHub): "Simple, flexible, interactive & powerful charts, maps and gauges for .Net, LiveCharts2 can now practically run everywhere Maui, Uno Platform, Blazor-wasm, WPF, WinForms, Xamarin, Avalonia, WinUI, UWP."
The LiveCharts2 site site has galleries, code and documentation that make it quick to add simple charts. The charts are fairly new, so I'm still testing/working, but I'm happy with the first round of results!
My happy feeling of being caught up on a personal project was recently crushed when I realized that dragging a file from my Android phone onto the Pointless Waymarks CMS resulted in ... nothing.
Files on my Windows computer and files from various network shares dropped without issues into the app - and the files from the Android phone worked fine in Windows Explorer between locations - but dragging Android to a WPF app didn't work.
I quickly turned to my buddy GitHub Copilot, who has given me some nice drag-and-drop answers in the past. My buddy produced some code that looked probable - and failed first try (hard to believe that this isn't a standard behavior most people experience these days). In a bit of behavior that might prove that GitHub Copilot is fundamentally human when asked about the exception from its code, the response was to blame the (probably corrupt) incoming data!
As with many things, it is a little hard to understand why the .NET drag-and-drop abstractions don't help more in this case and leaves you mucking around with FORMATETC and binary formats.
Before considering any changes I took a quick look at the details blocks of some well known photo sites: Flickr, 500px, SmugMug, PhotoPrism (you will have to find example pages to see the position and interaction associated with these).
There are nice details in all of the examples above but ultimately for the purposes and currently style of a Pointless Waymarks CMS site I think the basic setup of the current block holds up reasonably well.
But it is certainly not perfect and I did find some improvements to make:
Move the focal length to a more useful/logical position
Correct the Aperture from f to ƒ
Remove the 'Details:' text/header. In the big picture this below the fold block of data is obscure information for curious photographers. There could be some utility in links or other help for someone who is curious but doesn't understand - however the text 'Details:' was not helping anyone.
The most interesting programing detail in this update was cleaning up the Aperture presentation. I don't want to limit what can be entered as Aperture and instead just cleanup well known formats to get a consistent string as possible. A simple example might be turning f9.0 into ƒ/9. The code I came up with:
public static string ApertureCleanup(string? aperture)
{
if (string.IsNullOrWhiteSpace(aperture))
return string.Empty;
// Remove f, ƒ, f/ or ƒ/ at the start of the aperture string
var apertureForCleaning = aperture.Trim();
if (apertureForCleaning.StartsWith("f/", StringComparison.OrdinalIgnoreCase) || apertureForCleaning.StartsWith("ƒ/", StringComparison.OrdinalIgnoreCase))
apertureForCleaning = apertureForCleaning.Substring(2);
else if (apertureForCleaning.StartsWith("f", StringComparison.OrdinalIgnoreCase) || apertureForCleaning.StartsWith("ƒ", StringComparison.OrdinalIgnoreCase))
apertureForCleaning = apertureForCleaning.Substring(1);
if (decimal.TryParse(apertureForCleaning, out var apertureValue))
{
var cultureSeparator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;
var apertureStringDecimal = apertureValue.ToString(CultureInfo.CurrentCulture);
apertureStringDecimal = apertureStringDecimal.Contains(cultureSeparator) ? apertureStringDecimal.TrimEnd('0').TrimEnd(cultureSeparator.ToCharArray()) : apertureStringDecimal;
return $"ƒ/{apertureStringDecimal}";
}
// Return the original string if it is not a recognizable format and value
return aperture.TrimNullToEmpty();
}
The detail that 'got me' was turning the decimal to a no trailing zeros string. I thought the solution was a 'G0' format - but ".00009" would become 9E-05. As far as I know it is incredibly unlikely a valid Aperture value would ever hit that, but I ended up on Stack Overflow looking at c# - Remove trailing zeros. With all the format options available I was a bit surprised that the solution ultimately involved string manipulation...
The recent Gear Post post on PointlessWaymarks.com is the first time I wanted to present the Photo Details outside of a Photo Page/Post. Pushing the full Photo Details Block into a post would be too much of a distraction, so I added a photo variation that includes the Photo Details in the caption.
The detail variation is made available by referring to the photo with {{photowdetails ....}} instead of the normal {{photo ...}} bracket code - example below.
These changes are barely large enough to write about but photography and photographs are important to me. The Pointless Waymarks CMS does not create dedicated photography sites - it does support all the pieces that you might need to tell stories with photography: files and images to reference, posts to bring things together, maps and geographic information...
Bruce Percy creates stunning images and - unusual these days - actually writes frequently on www.brucepercy.co.uk. The photos and thoughtful words are exactly the content I wrote the Pointless Waymarks Feed Reader to follow - high-value content, not a stream of attention-economy clickbait.
But lately I haven't been able to keep up - great content or not Bruce Percy's posts have made my Feed Reader Items seem more like a list to manage than a source of daily inspiration.
As a .NET Developer I consider the Morning Dew by Alvin Ashcraft – Daily links for Windows and .NET developers essential reading. The daily links are an easy way to keep in touch with the constantly evolving world of .NET and occasionally link to some package or concept that are stunningly valuable - and that I might otherwise not have found.
The Morning Dew hasn't been in my Feed Reader list because with nearly every workday publication it makes more sense just to visit the website. There isn't any mystery about whether there is a new post, and I long ago gave up on keeping up with every single day.
I recently tried to add his site to my feeds via his Atom feed, but it only resulted in an error...
I initially imagined that the content I would follow in the Pointless Waymarks Windows Desktop Feed Reader would be infrequent enough that the program didn't need any options around automatically managing feed items. That has largely proved to be true, but some important outliers like the ones above have crept in, and it is now obvious that borrowing a few features found in other readers would be helpful - the Feeds now have options for:
Mark Read if More Than __ Items: Inspired by Bruce Percy this setting allows me to see some content but without the list ever growing too long if I don't catch up for a bit.
Auto Mark Read After __ Days: Inspired by Alvin Ashcraft's Morning Dew I'm using this set to '1' so that I only see the current post in my reader items - a nice compromise so that only the latest is ever in my morning reading.
Coding these features reminded me that I've been using FluentMigrator: Fluent migrations framework for .NET for over 4 years. I found FluentMigrator after being puzzled about how to make EF Core Migrations work nicely for my scenarios. In recent versions, EF Core Migrations would probably be less of a puzzle - both packages have improved. Some FluentMigrator code related to the changes above are included below. This simple usage/example isn't a tour-de-force (or maybe even best practices) example of FluentMigrator, but it is a simple pattern that has been very stable/easy/reliable for me over several years:
using System.Data;
using FluentMigrator;
namespace PointlessWaymarks.FeedReaderData.Migrations;
[Migration(202412260000)]
public class AddAutoMarkReadMigration : Migration
{
public override void Down()
{
throw new DataException($"No Down Available for Migration {nameof(AddAutoMarkReadMigration)}");
}
public override void Up()
{
if (!Schema.Table("Feeds").Column("AutoMarkReadAfterDays").Exists())
Execute.Sql(@"ALTER TABLE Feeds
ADD COLUMN AutoMarkReadAfterDays INTEGER NULL");
if (!Schema.Table("HistoricFeeds").Column("AutoMarkReadAfterDays").Exists())
Execute.Sql(@"ALTER TABLE HistoricFeeds
ADD COLUMN AutoMarkReadAfterDays INTEGER NULL");
}
}
Line 71 from Matt Payne's (feed)[https://www.mattpaynephotography.com/rss.xml] on 12/28/2024:
<title>Kidney Transplant and Artistic Vision: Tracey Halladay’s Powerful Photography Journey</title>
That line triggers a System.Xml.XmlException 'Reference to undeclared entity 'rsquo''. In many situations it could be productive to work with the producer of the XML to explore and resolve the error. In this situation I want to make the most of whatever is offered, so I added code to catch this specific error, parse out the entity that is causing problems, modify the feed, and try again.
/// <summary>
/// Extracts the undeclared entity from and XML exception message - the intent is that
/// this can then be removed from the XML content and the parsing retried.
/// </summary>
/// <param name="exception"></param>
/// <returns></returns>
private static (string entity, int lineNumber, int linePosition) ExtractUndeclaredEntityInformation(
XmlException exception)
{
var startIndex = exception.Message.IndexOf("undeclared entity '", StringComparison.Ordinal) +
"undeclared entity '".Length;
var endIndex = exception.Message.IndexOf("'", startIndex, StringComparison.Ordinal);
if (startIndex > 0 && endIndex > startIndex)
return ($"&{exception.Message[startIndex..endIndex]};", exception.LineNumber, exception.LinePosition - 1);
return (string.Empty, exception.LineNumber, exception.LinePosition - 1);
}
/// <summary>
/// Replaces a string in the content at the given line and position - but if the value to replace is not found at
/// the line and position, it replaces it everywhere in the content. This is very specific to parsing the feed
/// content and the fallback is just-in-case there is some unexpected difference between the line and position
/// from the exception and the parsing done in this method.
/// </summary>
/// <param name="content"></param>
/// <param name="toReplace"></param>
/// <param name="lineNumber"></param>
/// <param name="linePosition"></param>
/// <param name="replacement"></param>
/// <returns></returns>
private static string ReplaceStringAtLineAndPositionOrAllContentsIfNotFound(string content, string toReplace,
int lineNumber, int linePosition, string replacement)
{
var lines = content.Split('\n');
if (lineNumber - 1 < lines.Length)
{
var line = lines[lineNumber - 1];
if (linePosition - 1 < line.Length && line.Substring(linePosition - 1, toReplace.Length) == toReplace)
{
var start = line.Substring(0, linePosition - 1);
var end = line.Substring(linePosition - 1 + toReplace.Length);
lines[lineNumber - 1] = start + replacement + end;
return string.Join('\n', lines);
}
}
// If the value to replace is not found at the given line and position, replace it everywhere in the content
return content.Replace(toReplace, replacement);
}
XDocument? feedDoc = null; // 2.) read document to get the used encoding
var tryProcess = true;
var tryProcessCount = 0;
//2024-12-27: 50 is arbitrary...
while (tryProcess && tryProcessCount < 50)
{
tryProcessCount++;
tryProcess = false;
try
{
feedDoc = XDocument.Parse(feedContent);
}
catch (XmlException ex) when (ex.Message.Contains("Reference to undeclared entity"))
{
// Extract the undeclared entity from the exception message
var entityExceptionInformation = ExtractUndeclaredEntityInformation(ex);
if (!string.IsNullOrEmpty(entityExceptionInformation.entity))
{
var undeclaredEntities = new Dictionary<string, string>
{
{ "’", "’" },
{ "‘", "‘" },
{ "”", "”" },
{ "“", "“" },
{ " ", " " }
};
// Remove the specific undeclared entity and retry parsing
var replacement = undeclaredEntities.GetValueOrDefault(entityExceptionInformation.entity, " ");
feedContent = ReplaceStringAtLineAndPositionOrAllContentsIfNotFound(feedContent,
entityExceptionInformation.entity, entityExceptionInformation.lineNumber,
entityExceptionInformation.linePosition, replacement);
tryProcess = true;
}
else
{
throw;
}
}
}
With only one feed sample to test for the Undeclared Entity issues I can't say whether this is best/right/most useful/most durable - but this code solves the current test case!
If you have any comments or suggestions contact me at [email protected] - Pointless Waymarks Windows Desktop Feed Reader use-at-your-own-risk Beta Version Installers available here.