Posts in category “Programming”

Smart Row Selection: Maintaining Infragistics UltraGrid State After Refresh

... skipping 1000 words about bad solutions ...

The Clean/Best Solution

Here's a pattern that elegantly handles this situation:

public void RefreshGrid()
{
    // 1. Store the current entity before refresh
    SomeEntity currentEntity = null;
    if (dataGrid.ActiveRow != null)
    {
        currentEntity = dataGrid.ActiveRow.ListObject as SomeEntity;
    }

    // 2. Refresh the grid
    dataGrid.RefreshData();

    // 3. Restore selection using entity ID
    if (currentEntity != null)
    {
        dataGrid.ActiveRow = dataGrid.Rows.FirstOrDefault(r => 
            ((SomeEntity)r.ListObject).Id == currentEntity.Id);
    }
}

Why This Pattern Is Best Practice

  1. Type Safety: Using the strongly-typed entity object instead of raw values
  2. Identity-Based: Uses unique IDs instead of volatile row positions
  3. Null-Safe: Handles cases where no row is selected
  4. Concise: LINQ makes the code readable and maintainable
  5. Reliable: Works even if data order changes or rows are filtered

Implementing QuickFix/n Logging with NLog

Wen working with QuickFix/n ibrary , efficient logging is crucial for troubleshooting. Here's how to implement a custom logging solution that routes QuickFix logs through NLog to your ELK stack.

Key Components

  1. NLogAdapter: A custom adapter that implements QuickFix's ILog interface:
public class NLogAdapter(ILogger logger) : ILog
{
    private const string HeartbeatPattern = @"\x0135=0\x01";
    private static readonly Regex HeartbeatRegex = new(HeartbeatPattern, RegexOptions.Compiled);

    private static bool IsHeartBeat(string message) => HeartbeatRegex.IsMatch(message);

    public void OnIncoming(string message)
    {
        if (!IsHeartBeat(message))
        {
            logger.Info("Incoming: {Message}", message);
        }
    }
    // ... other implementations
}
  1. NLogQuickFixLogFactory: A factory class to create log instances:
public class NLogQuickFixLogFactory(ILog logger) : ILogFactory
{
    public ILog Create(SessionID sessionId) => logger;
    public ILog CreateNonSessionLog() => logger;
}

Implementation Steps

  1. Register Dependencies in your DI container:
builder.Services.AddSingleton<NLog.ILogger>(_ => LogManager.GetCurrentClassLogger());
builder.Services.AddSingleton<ILog, NLogAdapter>();
builder.Services.AddSingleton<ILogFactory, NLogQuickFixLogFactory>();
  1. Configure QuickFix to use the custom logger:
var initiator = new SocketInitiator(
    clientApp,
    storeFactory,
    sessionSettings,
    new NLogQuickFixLogFactory(quickfixLogger)  // Use custom logger injeted by ILog here 
);

Key Features

  • Heartbeat Filtering: Reduces log noise by filtering out FIX heartbeat messages
  • Structured Logging: Uses NLog's structured logging format for better parsing in ELK
  • Separation of Concerns: Cleanly separates QuickFix logging from application logging

Benefits

  1. Centralized logging in ELK stack
  2. Better debugging apabilities
  3. Reduced log volume through heartbeat filtering
  4. Consistent logging format across your application

Common NUnit `Assert` statements

Here’s a consolidated list of common NUnit Assert statements, categorized by their purpose. This should cover most of the common scenarios:


Basic Assertions

  • Equality:

    Assert.That(actual, Is.EqualTo(expected));
    Assert.That(actual, Is.Not.EqualTo(expected));
    
  • Boolean Conditions:

    Assert.That(condition, Is.True);
    Assert.That(condition, Is.False);
    
  • Null Checks:

    Assert.That(obj, Is.Null);
    Assert.That(obj, Is.Not.Null);
    

String Assertions

  • Contains:

    Assert.That(actualString, Does.Contain(substring));
    
  • Starts With / Ends With:

    Assert.That(actualString, Does.StartWith(prefix));
    Assert.That(actualString, Does.EndWith(suffix));
    
  • Empty or Not Empty:

    Assert.That(actualString, Is.Empty);
    Assert.That(actualString, Is.Not.Empty);
    
  • Matches Regex:

    Assert.That(actualString, Does.Match(regexPattern));
    

Collection Assertions

  • Contains Item:

    Assert.That(collection, Does.Contain(item));
    
  • Has Specific Count:

    Assert.That(collection, Has.Count.EqualTo(expectedCount));
    
  • Empty or Not Empty:

    Assert.That(collection, Is.Empty);
    Assert.That(collection, Is.Not.Empty);
    
  • Unique Items:

    Assert.That(collection, Is.Unique);
    

Numeric Assertions

  • Greater Than / Less Than:

    Assert.That(actual, Is.GreaterThan(expected));
    Assert.That(actual, Is.LessThan(expected));
    
  • Greater Than or Equal / Less Than or Equal:

    Assert.That(actual, Is.GreaterThanOrEqualTo(expected));
    Assert.That(actual, Is.LessThanOrEqualTo(expected));
    
  • In Range:

    Assert.That(actual, Is.InRange(lower, upper));
    

Type Assertions

  • Instance of Type:

    Assert.That(obj, Is.TypeOf<ExpectedType>());
    Assert.That(obj, Is.InstanceOf<ExpectedType>());
    
  • Assignable From:

    Assert.That(obj, Is.AssignableTo<ExpectedType>());
    

Exception Assertions

  • Throws Exception:

    Assert.Throws<ExpectedExceptionType>(() => { methodCall(); });
    
  • Throws Specific Exception with Condition:

    var ex = Assert.Throws<ExpectedExceptionType>(() => { methodCall(); });
    Assert.That(ex.Message, Does.Contain("expected message"));
    

Miscellaneous

  • Same Instance:

    Assert.That(actual, Is.SameAs(expected));
    Assert.That(actual, Is.Not.SameAs(expected));
    
  • Applies a Condition:

    Assert.That(collection, Has.Some.Matches<ExpectedType>(item => item.Condition));
    
  • Delayed Constraints (Asynchronous):

    Assert.That(() => condition, Is.True.After(500).PollEvery(50));
    
  • Group related assertions together to improve readability and reporting:

    Assert.Multiple(() =>
    {
        Assert.That(okResult, Is.Not.Null, "okResult should not be null");
        Assert.That(okResult.Value, Is.TypeOf<string>(), "Value should be of type string");
    });
    

= null! vs. = string.Empty: Best Practices for Model Class Initialization

The choice between = null! and = string.Empty depends on the context and the intent of the code. Here's a breakdown to help you decide:


= null!

When to Use:

  1. You Intend to Initialize the Field Later:

    • Use = null! when the property or field is expected to be set by a constructor, a method, or dependency injection after the object is created.

    • Example:

      public string Name { get; set; } = null!;
      
    • Why: null! tells the compiler to trust you that the value will be assigned before being accessed.

  2. Avoid Premature Default Values:

    • If a default value like string.Empty doesn't make sense for the context, null! keeps the intent clearer.
  3. For Models or Data Contracts:

    • In cases where fields are expected to match a schema or be deserialized from external sources, it might be better to indicate "unknown until set."

    • Example:

      public string Username { get; set; } = null!; // Will be deserialized later
      

= string.Empty

When to Use:

  1. Default Values Are Meaningful:

    • Use = string.Empty when an empty string is a valid and reasonable default.

    • Example:

      public string Name { get; set; } = string.Empty;
      
    • Why: This avoids null-checking or potential runtime errors when accessing the property.

  2. Simpler to Work With:

    • If the field/property is commonly accessed and null is not a meaningful value in your application, an empty string simplifies the logic (avoids extra null-checks).
  3. UI or Display-Friendly Fields:

    • For fields used in UI contexts, empty strings often make more sense than null:

      public string Description { get; set; } = string.Empty;
      

Key Differences

Aspect = null! = string.Empty
Purpose Indicates value will be set later. Provides an immediate, valid default.
Compiler Warnings Suppresses nullability warnings (unsafe). Avoids nullability warnings entirely.
Clarity Explicitly states "not initialized yet". Implies "initialized to empty value now".
Best Fit Models, external contracts, DI patterns. Readily usable properties or fields.

When to Avoid = null!

  • When it's unclear who or what will initialize the property.
  • When using null might lead to accidental runtime errors.
  • When the property will be frequently accessed before initialization.

Recommendation

  • Use = string.Empty when empty strings make sense as defaults and simplify code.
  • Use = null! when initialization will occur later, and null isn't a valid or meaningful runtime value.

You know who is the real author of this article, don't you? :P

Migrated your moments from HappyFeed to HappyNotes

First, get your moments data

(function () {
  const MAX_WAIT_TIME = 50000; // If the button is always not there, we wait at most 50 seconds.
  const CHECK_INTERVAL = 1000; // If the button is not there, we do the same check after 1 scond
  const buttonSelector = '#__next > main > div > div.MomentHistory_moment-history__inner__a9etG > button';

  // Initially finding the button
  const button = document.querySelector(buttonSelector);
  if (button) {
    button.click();
    console.log('Found the button and clicked');
  } else {
    console.log('No Load button at all');
    return
  }

  // 当前已等待的时间
  let elapsedTime = 0;

  // 定时器开始循环查找,立即获得 intervalId
  const intervalId = setInterval(() => {
    elapsedTime += CHECK_INTERVAL;

    // Searching the Load button
    const retryButton = document.querySelector(buttonSelector);
    if (retryButton) {
      retryButton.click();
      elapsedTime = 0
      console.log('Found the button and clicked');
    } else {
      console.log('Could not find the button, continue searching...');
    }

    // If we could no longer find the button in MAX_WAIT_TIME, we know the work has been done.
    if (elapsedTime >= MAX_WAIT_TIME) {
      console.error('Could not find the button any more, stopping....');
      clearInterval(intervalId); // Stop the interval
    }
  }, CHECK_INTERVAL); // check if we can find the button every second
})()

Second, extract the data into a JSON object from the long page we get by clicking the Load button again and again by the script above

const diaryEntries = [];
const listItems = document.querySelectorAll('.MomentListItem_moment-list__item__d5UJL');

// Helper function:Convert date into yyyy-MM-dd format
function formatDate(dateString) {
    const months = {
        January: "01", February: "02", March: "03", April: "04", May: "05", June: "06",
        July: "07", August: "08", September: "09", October: "10", November: "11", December: "12"
    };
    const [month, day, year] = dateString.split(" ");
    return `${year}-${months[month]}-${day.padStart(2, '0')}`;
}

listItems.forEach(item => {
    const dateElement = item.querySelector('.MomentListItem_moment-item__date__XfMA8');
    const contentElement = item.querySelector('.MomentListItem_moment-item__text__F5V2C > p');
    const photoElement = item.querySelector('.MomentImage_root__uLWy7 img');
    const videoElement = item.querySelector('.MomentListItem_moment-item__video__16FUa video');

    const entry = {
        pdate: dateElement ? formatDate(dateElement.textContent.replace('Posted ', '').trim()) : null,
        content: contentElement ? contentElement.textContent.trim() : '',
        photo: ''
    };

    if (photoElement) {
        entry.photo = photoElement.getAttribute('src');
    } else if (videoElement) {
        const thumbnail = videoElement.getAttribute('poster');
        entry.photo = thumbnail ? thumbnail.trim() : '';
    }

    diaryEntries.push(entry);
    diaryEntries.reverse(); // make sure the old data is at the beginning
});

Third, send the moments data to HappyNotes by the following script

// Your personal token
const token = "eyJh...";

// API endpoint
const apiUrl = "https://happynotes-api.shukebeta.com/note/post";

// Function to send a note
async function sendNote(note) {
    const contentWithPhoto = note.photo 
        ? `${note.content}\n\n ![image](${note.photo})`
        : note.content;

    const payload = {
        isprivate: true,
        isMarkdown: true,
        publishDateTime: note.pdate,
        timezoneId: "Pacific/Auckland",
        content: contentWithPhoto
    };

    try {
        const response = await fetch(apiUrl, {
            method: "POST",
            headers: {
                Authorization: `Bearer ${token}`,
                "Content-Type": "application/json"
            },
            body: JSON.stringify(payload)
        });

        if (!response.ok) {
            throw new Error(`HTTP error! Status: ${response.status}`);
        }

        console.log(`Note sent successfully: ${await response.text()}`);
    } catch (error) {
        console.error(`Error sending note: ${error.message}`);
    }
}

// Send notes one by one
(async () => {
    var date = ''
    var minutes = '00'
    for (const note of diaryEntries) {
        if (note.pdate != date) {
           date = note.pdate
           minutes = '00'
        } else {
           minutes = (+minutes + 10) + ''
        }
        note.pdate += ` 20:${minutes}:00`
        await sendNote(note);
    }
})();

You need to get the token by a manually login. Don't share the token to anyone else! Anyone who gets the token can access your account and data. Follow the following steps to get your personal token.

Open the DevTools on Chrome or Firefox, then do a fresh login.

image