Sharing Lightroom images in DNG with ability to undo/redo edits in another application?

I am contracted to receive a bunch of images from a photographer who has made edits to many of the images.
The photographer has exported a sample DNG file with me from Lightroom Classic 10.2 and I was able to open it in Lightroom v4 (2012 version). The image opens in Lightroom 4 with the photographer’s edits applied, and I am able to undo those edits in Lightroom. This is as desired.

If I understand the reference here correctly, it suggests that the photographer’s Lightroom is exporting DNG’s with the original RAW files with edit metadata (XMP) embedded within the DNG also? Is this true?

If I choose to edit the DNG in a non-Adobe application such as Luminar / Dark Table or another (eg https://shotkit.com/best-alternative-to-lightroom/) then i) will the DNG images open as if the photographer had made no edits (ie in RAW original) or would they open with the edits applied?
Are there any applications other than Lightroom in which I can undo/redo Lightroom edits if I already have the DNG?

object oriented – DataGridView Undo/Redo Manager in C#

While answering this code review question, I proposed a solution with List<DataTable>. I am trying to encapsulate this data structure into a “DataGridView Undo/Redo Manager” class DataGridViewManager and there are several build-in methods including IsUndoable, IsRedoable, Push, Redo, Undo, etc. The following methods are implemented.

  • IsUndoable method handles the process for checking if the previous states is available.
  • IsRedoable method handles the process for checking if the next states is available.
  • Push method handles the process for pushing new states.

The experimental implementation

The experimental implementation of DataGridViewManager is as below.

class DataGridViewManager
{
    Stack<DataTable> dtStack = new Stack<DataTable>();
    private int RecordIndex = 0;

    public DataGridViewManager()
    { }

    public DataGridViewManager Push(DataTable dataTable)
    {
        ClearUnnecessaryHistory();
        this.dtStack.Push(dataTable);
        return this;
    }

    public DataTable Redo()
    {
        return dtStack.ToList()(--RecordIndex);
    }

    public DataTable Undo()
    {
        return dtStack.ToList()(++RecordIndex);
    }

    public DataTable GetCurrentState()
    {
        return dtStack.ElementAt(RecordIndex);
    }

    public bool IsUndoable()
    {
        if (RecordIndex == this.dtStack.Count - 1)
            return false;
        else
            return true;
    }

    public bool IsRedoable()
    {
        if (RecordIndex == 0)
            return false;
        else
            return true;
    }

    private void ClearUnnecessaryHistory()
    {
        while (RecordIndex > 0)
        {
            dtStack.Pop();
            RecordIndex--;
        }
        return;
    }
}

Test cases

The usage of DataGridViewManager class is like:

public partial class Form1 : Form
{
    DataGridViewManager DataGridViewManager1 = new DataGridViewManager();

    public Form1()
    {
        InitializeComponent();

        //    Construct Columns
        dataGridView1.ColumnCount = 1;
        dataGridView1.Columns(0).Name = "0";

        dataGridView1.Rows.Add(20);// Add row

        DataGridViewManager1.Push(GetDataTableFromDGV(dataGridView1));
        UpdateBtnStatus();
    }

    public DataTable GetDataTableFromDGV(DataGridView dgv)
    {
        var dt = new DataTable();

        foreach (DataGridViewColumn column in dgv.Columns)
        {
            dt.Columns.Add(column.Name);
        }

        object() cellValues = new object(dgv.Columns.Count);

        foreach (DataGridViewRow row in dgv.Rows)
        {
            for (int i = 0; i < row.Cells.Count; i++)
            {
                cellValues(i) = row.Cells(i).Value;
            }
            dt.Rows.Add(cellValues);
        }
        return dt;
    }

    public void datatablaToDataGrid(DataGridView dgv, DataTable datatable)
    {
        for (int i = 0; i < datatable.Rows.Count; i++)
        {
            for (int j = 0; j < datatable.Columns.Count; j++)
            {
                dgv.Rows(i).Cells(j).Value = datatable.Rows(i)(j).ToString();
            }
        }
    }

    private void UpdateBtnStatus()
    {
        btn_Redo.Enabled = DataGridViewManager1.IsRedoable();
        btn_Undo.Enabled = DataGridViewManager1.IsUndoable();
        return;
    }

    private void btn_Undo_Click(object sender, EventArgs e)
    {
        datatablaToDataGrid(dataGridView1, DataGridViewManager1.Undo());
        UpdateBtnStatus();
    }

    private void btn_Redo_Click(object sender, EventArgs e)
    {
        datatablaToDataGrid(dataGridView1, DataGridViewManager1.Redo());
        UpdateBtnStatus();
    }

    private void dataGridView1_CellValidated(object sender, DataGridViewCellEventArgs e)
    {
        UpdateBtnStatus();

        DataGridView dgv = (DataGridView)sender;
        int r = e.RowIndex;
        int c = e.ColumnIndex;
        if (dgv.Rows(r).Cells(c).Value != null)
        {
            string dgvResult = dgv.Rows(r).Cells(c).Value.ToString();
            string dtResult = DataGridViewManager1.GetCurrentState().Rows(r)(c).ToString();
            if (dgvResult != dtResult)
            {
                DataGridViewManager1.Push(GetDataTableFromDGV(dataGridView1));
            }
        }
        UpdateBtnStatus();
    }
}

All suggestions are welcome.

winforms – Undo/Redo Functionality in DataGridView c#

Review

Welcome to Code Review. There are few suggestions as below.

Layout and formatting

Not sure why many newlines between the curly brackets and the method definition, if statements and for statements. For improving readability, those unnecessary newlines can be removed.

Magic numbers and List<DataTable>

I have no idea about why the initial value of counterUndo is set to 2 (in int counterUndo = 2) and why the inequality check if (_counterUndo != 2) is needed in doWhenClickedUndo method. Is the times of undo operation limited in 2? How about the case of more steps the user wants to undo? To solve this issue, I tried to use Stack<DataTable> instead of List<DataTable> so that the Push, Pop and First methods is available (Stack is useful for maintaining the historical sequence like the states of DataTable here). The following code is as an example implementation with Stack class.

public partial class Form1 : Form
{
    Stack<DataTable> dtStack = new Stack<DataTable>();
    int RecordIndex = 0;
    bool UndoRedo = false;

    public Form1()
    {
        InitializeComponent();

        //    Construct Columns
        dataGridView1.ColumnCount = 1;
        dataGridView1.Columns(0).Name = "0";

        dataGridView1.Rows.Add(20);// Add row

        dtStack.Clear();

        dtStack.Push(GetDataTableFromDGV(dataGridView1));
        UpdateBtnStatus();
    }

    public DataTable GetDataTableFromDGV(DataGridView dgv)
    {
        var dt = new DataTable();

        foreach (DataGridViewColumn column in dgv.Columns)
        {
            dt.Columns.Add(column.Name);
        }

        object() cellValues = new object(dgv.Columns.Count);

        foreach (DataGridViewRow row in dgv.Rows)
        {
            for (int i = 0; i < row.Cells.Count; i++)
            {
                cellValues(i) = row.Cells(i).Value;
            }
            dt.Rows.Add(cellValues);
        }
        return dt;
    }
    
    public void datatablaToDataGrid(DataGridView dgv, DataTable datatable)
    {
        for (int i = 0; i < datatable.Rows.Count; i++)
        {
            for (int j = 0; j < datatable.Columns.Count; j++)
            {
                dgv.Rows(i).Cells(j).Value = datatable.Rows(i)(j).ToString();
            }
        }
    }

    private void UpdateBtnStatus()
    {
        if (RecordIndex == this.dtStack.Count - 1)
            this.btn_Undo.Enabled = false;
        else
            this.btn_Undo.Enabled = true;

        if (RecordIndex == 0)
            this.btn_redo.Enabled = false;
        else
            this.btn_redo.Enabled = true;
    }

    private void btn_Undo_Click(object sender, EventArgs e)
    {
        UndoRedo = true;
        datatablaToDataGrid(dataGridView1, dtStack.ToList()(++RecordIndex));
        UpdateBtnStatus();
        UndoRedo = false;
    }

    private void btn_redo_Click(object sender, EventArgs e)
    {
        UndoRedo = true;
        datatablaToDataGrid(dataGridView1, dtStack.ToList()(--RecordIndex));
        UpdateBtnStatus();
        UndoRedo = false;
    }

    private void dataGridView1_CellValidated(object sender, DataGridViewCellEventArgs e)
    {
        if (UndoRedo)
            return;

        while (RecordIndex != 0)
        {
            dtStack.Pop();
            RecordIndex--;
        }

        DataGridView dgv = (DataGridView)sender;
        int r = e.RowIndex;
        int c = e.ColumnIndex;
        if (dgv.Rows(r).Cells(c).Value != null)
        {
            string dgvResult = dgv.Rows(r).Cells(c).Value.ToString();
            string dtResult = dtStack.First().Rows(r)(c).ToString();
            if (dgvResult != dtResult)
            {
                dtStack.Push(GetDataTableFromDGV(dataGridView1));
            }
        }
        UpdateBtnStatus();
    }
}

internationalisation – Why were Ctrl-Z and Ctrl-Y chosen as undo/redo shortcuts?

It is likely based on the key position of other commonly used actions (cut, copy, paste) on the keyboard.

Likely it all boils down to placement on a QWERTY keyboard.

(…)

From there, X and V are just adjacent keys, for Cut and Paste. It’s
simple to remember where they are, and you’ll build up muscle memory
if they’re close. (You’ll do it even if they aren’t, but if they’re
adjacent, it helps.) Eventually, when Mac and Windows word processors
(and other applications) started offering Undo as a feature, putting
them together still makes a kind of conceptual sense: these are basic
document editing features which work the same way across even very
different applications (“Undo” is undo in Word, or Adobe Illustrator,
for example, even if the content types are wildly different).

Source: Why was ‘Z’ chosen for the CTRL+Z/CMD+Z shortcut?

It’s also close to the control/command key, which makes it easy to use with one hand. Common actions (cut, copy, paste, undo) are used a lot, so it is important to have them close to the control/command key.

Undo-Redo for sections not visible to the user

In a multi-step task-based UI, imagine the flow below –

  1. User adds a task and a panel opens up to fill in the task details
  2. User fills the details and saves the task
  3. User closes this add-on panel
  4. User performs an undo for some reason
  5. The undo operation is applied to the closed task panel as that was the last action

However, this does not seem right as the action taken by undo isn’t in the user’s view. Thoughts?