瀏覽代碼

Converted DigitalFormsDashboard to CoreTableGrid

Kenric Nugteren 7 月之前
父節點
當前提交
e0fccaccc7

+ 3 - 17
prs.desktop/Dashboards/Common/DigitalFormsDashboard.xaml

@@ -17,23 +17,9 @@
 
         <wpf:QAGrid x:Name="QAGrid" Grid.Column="0" BorderThickness="0" MaxWidth="500" />
 
-        <syncfusion:SfDataGrid
-                    Background="White"
-                    x:Name="DataGrid"
-                    Grid.Column="1"
-                    AutoGenerateColumns="True"
-                    AutoGeneratingColumn="DataGrid_AutoGeneratingColumn"
-                    RowHeight="30"
-                    AllowSorting="False"
-                    SelectionUnit="Row"
-                    NavigationMode="Cell"
-                    SelectionMode="Extended"
-                    Margin="0,5,5,5"
-                    CellDoubleTapped="DataGrid_CellDoubleTapped"
-                    CellTapped="DataGrid_CellTapped"
-                    SelectionChanged="DataGrid_SelectionChanged"
-                    FilterRowPosition="FixedTop">
-        </syncfusion:SfDataGrid>
+        <local:DigitalFormsDashboardGrid x:Name="Grid" Grid.Column="1"
+                                         OnSelectItem="Grid_OnSelectItem"
+                                         OnCellDoubleClick="Grid_OnCellDoubleClick"/>
 
     </Grid>
 </UserControl>

+ 294 - 312
prs.desktop/Dashboards/Common/DigitalFormsDashboard.xaml.cs

@@ -44,8 +44,6 @@ using Image = System.Windows.Controls.Image;
 using Microsoft.Win32;
 using Syncfusion.CompoundFile.DocIO.Net;
 using System.IO;
-using com.healthmarketscience.jackcess.query;
-using sun.security.krb5.@internal;
 using Columns = InABox.Core.Columns;
 using PanelAction = InABox.Wpf.PanelAction;
 
@@ -75,19 +73,6 @@ public class DFFilter : BaseObject
     public string Filter { get; set; } = "";
 }
 
-internal class MileStoneImageConverter : IValueConverter
-{
-    public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
-    {
-        return Equals(value, DateTime.MinValue) ? null : Resources.milestone.AsBitmapImage();
-    }
-
-    public object? ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
-    {
-        return null;
-    }
-}
-
 public class DigitalFormsDashboardProperties : IUserConfigurationSettings, IDashboardProperties
 {
     public bool ShowJobFilter { get; set; } = false;
@@ -142,7 +127,7 @@ public partial class DigitalFormsDashboard : UserControl,
     {
         InitializeComponent();
 
-        DataGrid.RowStyleSelector = new RowStyleSelector();
+        Grid.OnGetRowStyle = Grid_OnGetRowStyle;
     }
 
     public void Setup()
@@ -173,6 +158,7 @@ public partial class DigitalFormsDashboard : UserControl,
                     return new Tuple<string, Type?, string>(appliesTo, null, x.Get<string>("Display"));
                 }
             }).ToList();
+        Grid.Categories = Categories;
 
         Jobs = results.Get<Job>().ToObjects<Job>().ToList();
         Jobs.Insert(0, new Job { ID = Guid.Empty, JobNumber = "ALL", Name = "All Jobs" });
@@ -597,7 +583,7 @@ public partial class DigitalFormsDashboard : UserControl,
         string changeableJobLink = "";
         string changeableScopeLink = "";
         
-        if ((FormType != null) && changeableLinks.TryGetValue(FormType, out Tuple<string,string> _tuple))
+        if ((FormType != null) && ChangeableLinks.TryGetValue(FormType, out Tuple<string,string> _tuple))
         {
             changeableJobLink = _tuple.Item1;
             changeableScopeLink = _tuple.Item2;
@@ -696,8 +682,8 @@ public partial class DigitalFormsDashboard : UserControl,
         var linkcolumn = GetJobLink("", FormType, true);
         if (!string.IsNullOrWhiteSpace(linkcolumn))
         {
-            linkcolumn = $"{linkcolumn.Replace(".","_")}_ID";
-            var jobid = DataGrid.SelectedItems.Select(x => (x as DataRowView)!.Row[linkcolumn]).FirstOrDefault();
+            linkcolumn = $"{linkcolumn}.ID";
+            var jobid = Grid.SelectedRows.Select(x => x[linkcolumn]).FirstOrDefault();
             if (jobid != null && !Guid.Equals(jobid, Guid.Empty))
             {
                 var scopes =Client.Query<JobScope>(
@@ -730,11 +716,10 @@ public partial class DigitalFormsDashboard : UserControl,
 
         string _idCol = FormType == typeof(JobForm)
             ? "ID"
-            : "Parent_ID";
-            
-        Guid[] _ids = DataGrid.SelectedItems.Select(x => (x as DataRowView)!.Row[_idCol]).Distinct()
-            .OfType<Guid>().ToArray();
-            
+            : "Parent.ID";
+
+        var _ids = Grid.SelectedRows.Select(x => x[_idCol]).OfType<Guid>().Distinct().ToArray();
+
         Progress.ShowModal("Updating Job Scopes", progress =>
         {
                 
@@ -774,10 +759,9 @@ public partial class DigitalFormsDashboard : UserControl,
 
             string _idCol = FormType == typeof(JobForm)
                 ? "ID"
-                : "Parent_ID";
+                : "Parent.ID";
             
-            Guid[] _ids = DataGrid.SelectedItems.Select(x => (x as DataRowView)!.Row[_idCol]).Distinct()
-                .OfType<Guid>().ToArray();
+            var _ids = Grid.SelectedRows.Select(x => x[_idCol]).OfType<Guid>().Distinct().ToArray();
             
             Progress.ShowModal("Updating Job Numbers", progress =>
             {
@@ -827,7 +811,7 @@ public partial class DigitalFormsDashboard : UserControl,
         switch (selection)
         {
             case Selection.Selected:
-                var formids = DataGrid.SelectedItems.Select(x => (x as DataRowView)!.Row["ID"]).ToArray();
+                var formids = Grid.SelectedRows.ToArray(x => x.Get<Guid>("ID"));
                 filter = Filter.Create<Entity>(FormType, x => x.ID).InList(formids);
                 break;
             case Selection.All:
@@ -909,13 +893,13 @@ public partial class DigitalFormsDashboard : UserControl,
             var result = dialog.ShowDialog();
             if(result == System.Windows.Forms.DialogResult.OK && !string.IsNullOrWhiteSpace(dialog.SelectedPath))
             {
-                var data = DataGrid.SelectedItems.OfType<DataRowView>().Select(x => x.Row).ToList();
+                var data = Grid.SelectedRows;
                 Progress.ShowModal("Saving forms", progress =>
                 {
                     foreach (var row in data)
                     {
-                        var id = (Guid)row["ID"];
-                        var number = (string)row["Number"];
+                        var id = row.Get<Guid>("ID");
+                        var number = row.Get<string>("Number");
                         progress.Report($"Saving form {number}");
                         var dataModel = new DigitalFormReportDataModel<TForm>(
                             new Filter<TForm>(x => x.ID).IsEqualTo(id),
@@ -936,50 +920,50 @@ public partial class DigitalFormsDashboard : UserControl,
 
     private void Export_Click()
     {
-        var formName = Regex.Replace(Form?.Description ?? "", "[^ a-zA-Z0-9]", "");
-        var filename = string.Format("{0} - {1} - {2:yyyy-MM-dd} - {3:yyyy-MM-dd}.xlsx", ParentType!.Name, formName, From, To);
-
-        var options = new ExcelExportingOptions();
-        options.ExcelVersion = ExcelVersion.Excel2013;
-        options.ExportStackedHeaders = true;
-        var excelEngine = DataGrid.ExportToExcel(DataGrid.View, options);
-        var workBook = excelEngine.Excel.Workbooks[0];
-        var sheet = workBook.Worksheets[0];
-        sheet.Name = "Summary";
-        sheet.UsedRange.AutofitRows();
-        sheet.UsedRange.AutofitColumns();
-
-        sheet = workBook.Worksheets.Create("Questions");
-        sheet.Move(0);
-        var questions = new Client<QAQuestion>().Query(new Filter<QAQuestion>(x => x.Form.ID).IsEqualTo(Form!.ID));
-        sheet.Range[1, 1].Text = Form?.Description ?? "";
-        sheet.Range[1, 1, 1, 3].Merge();
-        var i = 1;
-        foreach (var row in questions.Rows)
-            if (!row.Get<QAQuestion, QAAnswer>(x => x.Answer).Equals(QAAnswer.Comment))
-            {
-                sheet.Range[i + 2, 1].Text = string.Format("{0}.", i);
-                sheet.Range[i + 2, 2].Text = string.Format("{0}", row.Get<QAQuestion, string>(x => x.Question));
-                sheet.Range[i + 2, 3].Text = string.Format("[{0}]", row.Get<QAQuestion, string>(x => x.Code));
-                i++;
-            }
-
-        sheet.UsedRange.AutofitRows();
-        sheet.UsedRange.AutofitColumns();
-
-        try
-        {
-            workBook.SaveAs(filename);
-
-            var startInfo = new ProcessStartInfo(filename);
-            startInfo.Verb = "open";
-            startInfo.UseShellExecute = true;
-            Process.Start(startInfo);
-        }
-        catch
-        {
-            MessageBox.Show(string.Format("Unable to Save/Launch [{0}]!\n\nIs the file already open?", filename));
-        }
+        // var formName = Regex.Replace(Form?.Description ?? "", "[^ a-zA-Z0-9]", "");
+        // var filename = string.Format("{0} - {1} - {2:yyyy-MM-dd} - {3:yyyy-MM-dd}.xlsx", ParentType!.Name, formName, From, To);
+
+        // var options = new ExcelExportingOptions();
+        // options.ExcelVersion = ExcelVersion.Excel2013;
+        // options.ExportStackedHeaders = true;
+        // var excelEngine = DataGrid.ExportToExcel(DataGrid.View, options);
+        // var workBook = excelEngine.Excel.Workbooks[0];
+        // var sheet = workBook.Worksheets[0];
+        // sheet.Name = "Summary";
+        // sheet.UsedRange.AutofitRows();
+        // sheet.UsedRange.AutofitColumns();
+
+        // sheet = workBook.Worksheets.Create("Questions");
+        // sheet.Move(0);
+        // var questions = new Client<QAQuestion>().Query(new Filter<QAQuestion>(x => x.Form.ID).IsEqualTo(Form!.ID));
+        // sheet.Range[1, 1].Text = Form?.Description ?? "";
+        // sheet.Range[1, 1, 1, 3].Merge();
+        // var i = 1;
+        // foreach (var row in questions.Rows)
+        //     if (!row.Get<QAQuestion, QAAnswer>(x => x.Answer).Equals(QAAnswer.Comment))
+        //     {
+        //         sheet.Range[i + 2, 1].Text = string.Format("{0}.", i);
+        //         sheet.Range[i + 2, 2].Text = string.Format("{0}", row.Get<QAQuestion, string>(x => x.Question));
+        //         sheet.Range[i + 2, 3].Text = string.Format("[{0}]", row.Get<QAQuestion, string>(x => x.Code));
+        //         i++;
+        //     }
+
+        // sheet.UsedRange.AutofitRows();
+        // sheet.UsedRange.AutofitColumns();
+
+        // try
+        // {
+        //     workBook.SaveAs(filename);
+
+        //     var startInfo = new ProcessStartInfo(filename);
+        //     startInfo.Verb = "open";
+        //     startInfo.UseShellExecute = true;
+        //     Process.Start(startInfo);
+        // }
+        // catch
+        // {
+        //     MessageBox.Show(string.Format("Unable to Save/Launch [{0}]!\n\nIs the file already open?", filename));
+        // }
     }
 
     private void ManageFilters_Click()
@@ -1090,14 +1074,30 @@ public partial class DigitalFormsDashboard : UserControl,
     private DateTime From { get; set; }
     private DateTime To { get; set; }
     private bool IsEntityForm { get; set; }
-    private Type? ParentType { get; set; }
-    private Type? FormType { get; set; }
+    private Type? _parentType;
+    private Type? ParentType
+    {
+        get => _parentType;
+        set
+        {
+            _parentType = value;
+            Grid.ParentType = value;
+        }
+    }
+    private Type? _formType;
+    private Type? FormType
+    {
+        get => _formType;
+        set
+        {
+            _formType = value;
+            Grid.FormType = value;
+        }
+    }
     private DigitalForm? Form { get; set; }
     private IFilter? CustomFilter { get; set; }
     private int? CustomFilterIndex { get; set; }
 
-    private readonly Dictionary<string, string> QuestionCodes = new();
-
     private static int WeekDay(DateTime date)
     {
         if (date.DayOfWeek == DayOfWeek.Sunday)
@@ -1174,7 +1174,7 @@ public partial class DigitalFormsDashboard : UserControl,
 
     private static Dictionary<string, Tuple<Type, Type>>? FormInstanceTypes;
 
-    private static readonly Dictionary<Type, List<Tuple<string, string>>> parentColumns = new()
+    public static readonly Dictionary<Type, List<Tuple<string, string>>> ParentColumns = new()
     {
         { typeof(Kanban), new() { new("Parent.Number", "Task No") } },
         { typeof(JobITP), new() { new("Parent.Code", "Code") } },
@@ -1183,13 +1183,13 @@ public partial class DigitalFormsDashboard : UserControl,
         { typeof(PurchaseOrderItem), new() { new("Parent.PONumber", "PO No") } }
     };
 
-    private static Dictionary<Type, Tuple<string, string>> changeableLinks = new()
+    public static Dictionary<Type, Tuple<string, string>> ChangeableLinks = new()
     {
         { typeof(JobForm), new( "Parent.ID", "Scope.ID") },
         { typeof(AssignmentForm), new( "JobLink.ID", "JobScope.ID") },
         { typeof(KanbanForm), new( "JobLink.ID", "") }
     };
-    
+
     private static bool CategoryToType(string category, [NotNullWhen(true)] out Type? formType, [NotNullWhen(true)] out Type? parentType)
     {
         if (FormInstanceTypes == null)
@@ -1247,7 +1247,7 @@ public partial class DigitalFormsDashboard : UserControl,
 
     #endregion
 
-    private string GetJobLink(string prefix, Type type, bool recursive)
+    private static string GetJobLink(string prefix, Type type, bool recursive)
     {
         if (type == null)
             return "null";
@@ -1270,7 +1270,7 @@ public partial class DigitalFormsDashboard : UserControl,
         return "";
     }
     
-    private string GetJobScopeLink(string prefix, Type type, int recurselevel)
+    private static string GetJobScopeLink(string prefix, Type type, int recurselevel)
     {
         var props = type.GetProperties().Where(x =>
             x.PropertyType.BaseType != null && x.PropertyType.BaseType.IsGenericType &&
@@ -1295,10 +1295,10 @@ public partial class DigitalFormsDashboard : UserControl,
     /// Find a link from the form type to an associated <see cref="Job"/>, allowing us to filter based on jobs.
     /// </summary>
     /// <returns>The property name of the <see cref="JobLink"/>.</returns>
-    private string GetJobLink<T>(bool recursive) where T : IDigitalFormInstance
+    private static string GetJobLink<T>(bool recursive) where T : IDigitalFormInstance
         => GetJobLink("", typeof(T), recursive);
     
-    private string GetJobScopeLink<T>(int recurselevel) where T : IDigitalFormInstance
+    private static string GetJobScopeLink<T>(int recurselevel) where T : IDigitalFormInstance
         => GetJobScopeLink("", typeof(T), recurselevel);
 
     private IKeyedQueryDef GetFormQuery<T>()
@@ -1349,7 +1349,7 @@ public partial class DigitalFormsDashboard : UserControl,
         var parentcols = LookupFactory.DefineColumns(ParentType!);
         foreach (var col in parentcols.ColumnNames())
             columns.Add("Parent." + col);
-        if (parentColumns.TryGetValue(ParentType!, out var pColumns))
+        if (ParentColumns.TryGetValue(ParentType!, out var pColumns))
         {
             foreach (var (field, name) in pColumns)
                 columns.Add(field);
@@ -1374,66 +1374,68 @@ public partial class DigitalFormsDashboard : UserControl,
 
     private void LoadDataIntoGrid(List<DigitalFormVariable> variables, List<QAQuestion> questions, CoreTable formData, List<string> additionalColumns, CoreTable? jobITPs)
     {
-        var data = new DataTable();
-        data.Columns.Add("ID", typeof(Guid));
-        data.Columns.Add("Form_ID", typeof(Guid));
-        data.Columns.Add("Parent_ID", typeof(Guid));
-        data.Columns.Add("Location_Timestamp", typeof(DateTime));
-        data.Columns.Add("Location_Latitude", typeof(double));
-        data.Columns.Add("Location_Longitude", typeof(double));
-        data.Columns.Add("FormData", typeof(string));
-        
-        data.Columns.Add("Number", typeof(string));
+        var cData = new CoreTable();
+        cData.AddColumn<Guid>("ID");
+        cData.AddColumn<Guid>("Form.ID");
+        cData.AddColumn<Guid>("Parent.ID");
+        cData.AddColumn<DateTime>("Location.Timestamp");
+        cData.AddColumn<double>("Location.Latitude");
+        cData.AddColumn<double>("Location.Longitude");
+        cData.AddColumn<string>("FormData");
 
-        if (parentColumns.TryGetValue(ParentType!, out var pColumns))
+        cData.AddColumn<string>("Number");
+
+        if (ParentColumns.TryGetValue(ParentType!, out var pColumns))
         {
             foreach (var (field, name) in pColumns)
-                data.Columns.Add(field.Replace(".","_"), typeof(string));
+                cData.AddColumn<string>(field);
         }
         
         var jobLink = GetJobLink("",FormType!,true);
+        Grid.JobLink = jobLink;
         if (!string.IsNullOrWhiteSpace(jobLink))
         {
-            if (!string.Equals(jobLink,"Parent"))
-                data.Columns.Add(jobLink.Replace(".","_") + "_ID", typeof(Guid));
-            data.Columns.Add(jobLink.Replace(".","_") + "_JobNumber", typeof(string));
+            if (!string.Equals(jobLink, "Parent"))
+                cData.AddColumn<Guid>(jobLink + ".ID");
+            cData.AddColumn<string>(jobLink + ".JobNumber");
         }
 
         var jobScopeLink = GetJobScopeLink("",FormType!,FormType == typeof(JobForm) ? 0 : 1);
+        Grid.JobScopeLink = jobLink;
         if (!string.IsNullOrWhiteSpace(jobScopeLink))
         {
-            data.Columns.Add(jobScopeLink.Replace(".","_") + "_ID", typeof(Guid));
-            data.Columns.Add(jobScopeLink.Replace(".","_") + "_Number", typeof(string));
+            cData.AddColumn<Guid>(jobScopeLink + ".ID");
+            cData.AddColumn<string>(jobScopeLink + ".Number");
         }
-        
 
-        data.Columns.Add("Description", typeof(string));
-        
-        data.Columns.Add("Parent_Description", typeof(string));
-        data.Columns.Add("Created", typeof(DateTime));
-        data.Columns.Add("Created By", typeof(string));
-        data.Columns.Add("Completed", typeof(DateTime));
-        data.Columns.Add("Completed By", typeof(string));
+        cData.AddColumn<string>("Description");
+        cData.AddColumn<string>("Parent.Description");
+
+        cData.AddColumn<DateTime>("Created");
+        cData.AddColumn<string>("Created By");
+        cData.AddColumn<DateTime>("Completed");
+        cData.AddColumn<string>("Completed By");
+
         if (IsEntityForm)
-            data.Columns.Add("Processed", typeof(bool));
+            cData.AddColumn<bool>("Processed");
 
-        if (variables.Any())
+        if (variables.Count != 0)
         {
             foreach (var variable in variables)
             {
                 var code = variable.Code.Replace("/", " ");
-                QuestionCodes[code] = Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase(code.ToLower());
+                Grid.QuestionCodes[code] = Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase(code.ToLower());
                 try
                 {
-                    data.Columns.Add(code, typeof(string));
+                    cData.AddColumn<string>(code);
                 }
-                catch (DuplicateNameException)
+                catch (DuplicateNameException e)
                 {
-                    MessageBox.Show($"Error: duplicate variable code {code}");
+                    MessageWindow.ShowError($"Error: duplicate variable code {code}", e, title: "Duplicate code");
                 }
             }
         }
-        else if (questions.Any())
+        else if (questions.Count != 0)
         {
             Questions = questions;
             Progress.SetMessage("Loading Checks");
@@ -1447,9 +1449,9 @@ public partial class DigitalFormsDashboard : UserControl,
                 var id = question.ID.ToString();
                 if (!question.Answer.Equals(QAAnswer.Comment))
                 {
-                    data.Columns.Add(id, typeof(string));
+                    cData.AddColumn<string>(id);
                     var code = question.Code;
-                    QuestionCodes[id] = string.IsNullOrEmpty(code) ? string.Format("{0}.", i) : code;
+                    Grid.QuestionCodes[id] = string.IsNullOrEmpty(code) ? string.Format("{0}.", i) : code;
                     i++;
                 }
             }
@@ -1460,14 +1462,14 @@ public partial class DigitalFormsDashboard : UserControl,
             var form = (row.ToObject(FormType!) as IDigitalFormInstance)!;
             if (true) //(!string.IsNullOrWhiteSpace(form.FormData))
             {
-                var dataRow = data.NewRow();
+                var dataRow = cData.NewRow();
 
                 dataRow["ID"] = form.ID;
-                dataRow["Form_ID"] = form.Form.ID;
-                dataRow["Parent_ID"] = form.ParentID();
-                dataRow["Location_Timestamp"] = form.Location.Timestamp;
-                dataRow["Location_Latitude"] = form.Location.Latitude;
-                dataRow["Location_Longitude"] = form.Location.Longitude;
+                dataRow["Form.ID"] = form.Form.ID;
+                dataRow["Parent.ID"] = form.ParentID();
+                dataRow["Location.Timestamp"] = form.Location.Timestamp;
+                dataRow["Location.Latitude"] = form.Location.Latitude;
+                dataRow["Location.Longitude"] = form.Location.Longitude;
                 dataRow["FormData"] = form.FormData;
                 dataRow["Number"] = form.Number;
                 dataRow["Description"] = form.Description;
@@ -1480,7 +1482,7 @@ public partial class DigitalFormsDashboard : UserControl,
                         desc.Add(val.ToString() ?? "");
                 }
 
-                dataRow["Parent_Description"] = string.Join(" : ", desc);
+                dataRow["Parent.Description"] = string.Join(" : ", desc);
 
                 dataRow["Created"] = (form as Entity)!.Created.IsEmpty()
                     ? form.FormStarted
@@ -1506,23 +1508,23 @@ public partial class DigitalFormsDashboard : UserControl,
                 if (pColumns != null)
                 {
                     foreach (var (field, name) in pColumns)
-                        dataRow[field.Replace(".","_")] = $"{row[field]}";
+                        dataRow[field] = $"{row[field]}";
                 }
 
                 if (!string.IsNullOrWhiteSpace(jobLink))
                 {
-                    dataRow[jobLink.Replace(".","_") + "_ID"] = row[jobLink + ".ID"];
-                    dataRow[jobLink.Replace(".","_") + "_JobNumber"] = row[jobLink + ".JobNumber"]?.ToString();
+                    dataRow[jobLink + ".ID"] = row[jobLink + ".ID"];
+                    dataRow[jobLink + ".JobNumber"] = row[jobLink + ".JobNumber"]?.ToString();
                 }
 
                 if (!string.IsNullOrWhiteSpace(jobScopeLink))
                 {
-                    dataRow[jobScopeLink.Replace(".","_") + "_ID"] = row[jobScopeLink + ".ID"];
-                    dataRow[jobScopeLink.Replace(".","_") + "_Number"] = row[jobScopeLink + ".Number"]?.ToString();
+                    dataRow[jobScopeLink + ".ID"] = row[jobScopeLink + ".ID"];
+                    dataRow[jobScopeLink + ".Number"] = row[jobScopeLink + ".Number"]?.ToString();
                 }
 
                 var bHasData = false;
-                if (variables.Any())
+                if (variables.Count != 0)
                 {
                     var dict = Serialization.Deserialize<Dictionary<string, object?>>(form.FormData);
                     if (dict is not null)
@@ -1533,7 +1535,7 @@ public partial class DigitalFormsDashboard : UserControl,
                             var value = variable.Deserialize(storage.GetEntry(variable.Code));
                             var format = variable.FormatValue(value);
                             var sKey = variable.Code.Replace("/", " ");
-                            if (data.Columns.Contains(sKey))
+                            if (cData.HasColumn(sKey))
                             {
                                 dataRow[sKey] = format;
                                 bHasData = true;
@@ -1546,23 +1548,24 @@ public partial class DigitalFormsDashboard : UserControl,
                     var dict = Serialization.Deserialize<Dictionary<Guid, object>>(form.FormData);
                     if (dict is not null)
                         foreach (var key in dict.Keys)
-                            if (data.Columns.Contains(key.ToString()))
+                        {
+                            var colName = key.ToString();
+                            if (cData.HasColumn(colName))
                             {
-                                dataRow[key.ToString()] = dict[key];
+                                dataRow[colName] = dict[key];
                                 bHasData = true;
                             }
+                        }
                 }
 
-
-                //if (bHasData)
-                data.Rows.Add(dataRow);
+                cData.Rows.Add(dataRow);
             }
         }
-        DataGrid.ItemsSource = data;
+        Grid.ItemsSource = cData;
 
-        IsQAForm = !variables.Any() && questions.Any();
+        IsQAForm = variables.Count == 0 && questions.Count != 0;
         QAGrid.Visibility = IsQAForm ? Visibility.Visible : Visibility.Collapsed;
-        DataGrid.Visibility = Visibility.Visible;
+        Grid.Visibility = Visibility.Visible;
     }
 
     private void RefreshData<TForm>()
@@ -1606,12 +1609,11 @@ public partial class DigitalFormsDashboard : UserControl,
         Questions.Clear();
         QAGrid.Clear();
         QAGrid.LoadChecks("", Array.Empty<QAQuestion>(), new Dictionary<Guid, object>());
-        DataGrid.ItemsSource = null;
 
         if (ParentType is null || FormType is null || Form is null || Form.ID == Guid.Empty)
         {
             QAGrid.Visibility = Visibility.Collapsed;
-            DataGrid.Visibility = Visibility.Collapsed;
+            Grid.Visibility = Visibility.Collapsed;
             return;
         }
 
@@ -1625,149 +1627,16 @@ public partial class DigitalFormsDashboard : UserControl,
 
     #region DataGrid Configuration
 
-    private class RowStyleSelector : StyleSelector
-    {
-        private Style NormalStyle;
-
-        private Style IncompleteStyle;
-
-        public RowStyleSelector()
-        {
-            NormalStyle = new Style();
-            NormalStyle.Setters.Add(new Setter(VirtualizingCellsControl.BackgroundProperty, new SolidColorBrush(Colors.White)));
-
-            IncompleteStyle = new Style();
-            IncompleteStyle.Setters.Add(new Setter(VirtualizingCellsControl.BackgroundProperty, new SolidColorBrush(Colors.LightSalmon)));
-        }
-
-        public override Style SelectStyle(object item, DependencyObject container)
-        {
-            var row = (item as DataRowBase)?.RowData as DataRowView;
-            if (row is null) return NormalStyle;
-
-            return (DateTime)row["Completed"] == DateTime.MinValue
-                ? IncompleteStyle
-                : NormalStyle;
-        }
-    }
-
-    private void DataGrid_AutoGeneratingColumn(object sender, Syncfusion.UI.Xaml.Grid.AutoGeneratingColumnArgs e)
+    private DynamicGridStyle Grid_OnGetRowStyle(CoreRow row, DynamicGridStyle defaultstyle)
     {
-
-        var jobLink = GetJobLink("", FormType, true) ?? "";
-        if (!string.IsNullOrWhiteSpace(jobLink))
-            jobLink = $"{jobLink.Replace(".","_")}_JobNumber";
-        
-        var jobScopeLink = GetJobScopeLink("", FormType, FormType == typeof(JobForm) ? 0 : 1) ?? "";
-        if (!string.IsNullOrWhiteSpace(jobScopeLink))
-                jobScopeLink = $"{jobScopeLink.Replace(".","_")}_Number";
-        
-        e.Column.TextAlignment = TextAlignment.Center;
-        e.Column.HorizontalHeaderContentAlignment = HorizontalAlignment.Center;
-        e.Column.ColumnSizer = GridLengthUnitType.None;
-        e.Column.ImmediateUpdateColumnFilter = true;
-        e.Column.FilterRowCondition = FilterRowCondition.Contains;
-        e.Column.FilterRowOptionsVisibility = Visibility.Collapsed;
-
-        var value = (e.Column.ValueBinding as Binding)!;
-        if (value.Path.Path.Equals("ID") || value.Path.Path.Equals("Form_ID") || value.Path.Path.Equals("Parent_ID") ||
-            value.Path.Path.Equals("FormData") || value.Path.Path.Equals("Location_Latitude") || value.Path.Path.Equals("Location_Longitude"))
-        {
-            e.Cancel = true;
-        }
-        else if (value.Path.Path.Equals("Number"))
-        {
-            e.Column.Width = 80;
-            e.Column.HeaderStyle = Application.Current.Resources["TemplateHeaderStyle"] as Style;
-        }
-        else if (value.Path.Path.Equals("Description"))
-        {
-            e.Column.Width = 250;
-            e.Column.HeaderStyle = Application.Current.Resources["TemplateHeaderStyle"] as Style;
-            e.Column.TextAlignment = TextAlignment.Left;
-            e.Column.HorizontalHeaderContentAlignment = HorizontalAlignment.Left;
-        }
-        else if (value.Path.Path.Equals("Location_Timestamp"))
-        {
-            e.Column = new GridImageColumn();
-            e.Column.Width = DataGrid.RowHeight;
-            e.Column.HeaderStyle = Application.Current.Resources["TemplateHeaderStyle"] as Style;
-            e.Column.HeaderText = "";
-            e.Column.Padding = new Thickness(4);
-            e.Column.ValueBinding = new Binding
-            {
-                Path = new PropertyPath(value.Path.Path),
-                Converter = new MileStoneImageConverter()
-            };
-            e.Column.MappingName = "Location_Timestamp";
-        }
-        else if (ParentType is not null && parentColumns.TryGetValue(ParentType, out var pColumns) && pColumns.Any(x => x.Item1.Replace(".","_").Equals(value.Path.Path)))
-        {
-            e.Column.ColumnSizer = GridLengthUnitType.Auto;
-            e.Column.HeaderStyle = Application.Current.Resources["TemplateHeaderStyle"] as Style;
-            e.Column.HeaderText = pColumns.FirstOrDefault(x => x.Item1.Replace(".","_").Equals(value.Path.Path))?.Item2 ?? "";
-        }
-        else if (value.Path.Path.Equals(jobLink))
-        {
-            e.Column.Width = 60;
-            e.Column.HeaderStyle = Application.Current.Resources["TemplateHeaderStyle"] as Style;
-            e.Column.HeaderText = "Job No";
-        }
-        else if (value.Path.Path.Equals(jobScopeLink))
-        {
-            e.Column.Width = 50;
-            e.Column.HeaderStyle = Application.Current.Resources["TemplateHeaderStyle"] as Style;
-            e.Column.HeaderText = "Scope";
-        }
-        else if (value.Path.Path.Equals("Parent_Description"))
-        {
-            e.Column.HeaderText = Categories.FirstOrDefault(x => x.Item2 == FormType)?.Item3 ?? "Parent";
-            e.Column.TextAlignment = TextAlignment.Left;
-            e.Column.HorizontalHeaderContentAlignment = HorizontalAlignment.Left;
-            e.Column.Width = 250;
-            e.Column.HeaderStyle = Application.Current.Resources["TemplateHeaderStyle"] as Style;
-        }
-        else if (value.Path.Path.Equals("Completed"))
-        {
-            e.Column.Width = 100;
-            e.Column.HeaderStyle = Application.Current.Resources["TemplateHeaderStyle"] as Style;
-            (e.Column as GridDateTimeColumn)!.DisplayBinding = new Binding {
-                Path = new PropertyPath(value.Path.Path),
-                Converter = new DateTimeToStringConverter("dd MMM yy hh:mm")
-            };
-        }
-        else if (value.Path.Path.Equals("Completed By"))
-        {
-            e.Column.Width = 100;
-            e.Column.HeaderStyle = Application.Current.Resources["TemplateHeaderStyle"] as Style;
-        }
-        else if (value.Path.Path.Equals("Processed"))
-        {
-            e.Column.Width = 100;
-            e.Column.HeaderStyle = Application.Current.Resources["TemplateHeaderStyle"] as Style;
-        }
-        else if (value.Path.Path.Equals("Created By"))
+        if (row.Get<DateTime>("Completed") == DateTime.MinValue)
         {
-            e.Column.Width = 100;
-            e.Column.HeaderStyle = Application.Current.Resources["TemplateHeaderStyle"] as Style;
+            return new DynamicGridRowStyle(defaultstyle) { Background = Colors.LightSalmon.ToBrush() };
         }
-        else if (value.Path.Path.Equals("Created"))
-        {
-            e.Column.Width = 100;
-            e.Column.HeaderStyle = Application.Current.Resources["TemplateHeaderStyle"] as Style;
-            (e.Column as GridDateTimeColumn)!.DisplayBinding = new Binding {
-                Path = new PropertyPath(value.Path.Path),
-                Converter = new DateTimeToStringConverter("dd MMM yy hh:mm")
-            };
-        }
-        else if (QuestionCodes.TryGetValue(e.Column.MappingName, out var header))
+        else
         {
-            e.Column.Width = 100;
-            e.Column.HeaderStyle = Application.Current.Resources["TemplateHeaderStyle"] as Style;
-            e.Column.HeaderText = header;
+            return defaultstyle;
         }
-        else
-            e.Cancel = true;
     }
 
     private Entity? GetEntityForm<T>(Guid id) where T : Entity, IDigitalFormInstance, IRemotable, IPersistent, new()
@@ -1779,22 +1648,26 @@ public partial class DigitalFormsDashboard : UserControl,
             columns).Rows.FirstOrDefault()?.ToObject<T>();
     }
 
-    private void DataGrid_CellDoubleTapped(object sender, Syncfusion.UI.Xaml.Grid.GridCellDoubleTappedEventArgs e)
-    {
-        if (e.RowColumnIndex.RowIndex < 2)
-            return;
-        var rowOffset = -2;
+    #endregion
 
-        //var table = (DataGrid.ItemsSource as DataTable)!;
-        var row = (e.Record as DataRowView)?.Row;
-        if (row == null)
+    private void Grid_OnSelectItem(object sender, DynamicGridSelectionEventArgs e)
+    {
+        var linkcolumn = GetJobLink("", FormType, true);
+        if (!string.IsNullOrWhiteSpace(linkcolumn) && _jobScopeAction != null)
         {
-            MessageBox.Show($"Unexpected Record type ({e.Record?.GetType().EntityName() ?? "NULL"}");
-            return;
+            linkcolumn = $"{linkcolumn}.ID";
+            var jobids = (e.Rows ?? []).Select(x => x[linkcolumn]).Distinct().ToArray();
+            _jobScopeAction.IsEnabled = jobids.Length == 1;
         }
-        var formid = (Guid)row["Form_ID"];
-        var formdata = (string)row["FormData"];
-        var id = (Guid)row["ID"];
+    }
+
+    private void Grid_OnCellDoubleClick(object sender, DynamicGridCellClickEventArgs args)
+    {
+        if (args.Row is null) return;
+
+        var formid = args.Row.Get<Guid>("Form.ID");
+        var formdata = args.Row.Get<string>("FormData");
+        var id = args.Row.Get<Guid>("ID");
 
         if (FormType is null) return;
 
@@ -1836,30 +1709,139 @@ public partial class DigitalFormsDashboard : UserControl,
             }
         }
     }
+}
+
+public class DigitalFormsDashboardGrid : CoreTableGrid
+{
+    public string? JobLink { get; set; }
+    public string? JobScopeLink { get; set; }
+
+    public Type? ParentType { get; set; }
+
+    public Type? FormType { get; set; }
+
+    public List<Tuple<string, Type?, string>> Categories { get; set; } = new();
+
+    public readonly Dictionary<string, string> QuestionCodes = new();
+
+    protected override void Init()
+    {
+        base.Init();
+
+        ActionColumns.Add(new DynamicImageColumn(Location_Image, Location_Click) { Position = DynamicActionColumnPosition.Start });
+    }
 
-    private void DataGrid_CellTapped(object sender, Syncfusion.UI.Xaml.Grid.GridCellTappedEventArgs e)
+    protected override void DoReconfigure(DynamicGridOptions options)
     {
-        if (e.RowColumnIndex.ColumnIndex == 0)
-        {
-            var timestamp = (DateTime)(e.Record as DataRowView)!.Row["Location_Timestamp"];
-            var latitude = (double)(e.Record as DataRowView)!.Row["Location_Latitude"];
-            var longitude = (double)(e.Record as DataRowView)!.Row["Location_Longitude"];
+        base.DoReconfigure(options);
 
-            var form = new MapForm(latitude, longitude, timestamp);
-            form.ShowDialog();
-        }
+        options.FilterRows = true;
     }
 
-    #endregion
+    private bool Location_Click(CoreRow? row)
+    {
+        if (row is null) return false;
+
+        var timestamp = row.Get<DateTime>("Location.Timestamp");
+        if (timestamp == DateTime.MinValue) return false;
+
+        var latitude = row.Get<double>("Location.Latitude");
+        var longitude = row.Get<double>("Location.Longitude");
 
-    private void DataGrid_SelectionChanged(object? sender, GridSelectionChangedEventArgs e)
+        var form = new MapForm(latitude, longitude, timestamp);
+        form.ShowDialog();
+
+        return false;
+    }
+
+    private BitmapImage? Location_Image(CoreRow? row)
     {
-        var linkcolumn = GetJobLink("", FormType, true);
-        if (!string.IsNullOrWhiteSpace(linkcolumn) && _jobScopeAction != null)
+        return row?.Get<DateTime>("Location.Timestamp") == DateTime.MinValue ? null : PRSDesktop.Resources.milestone.AsBitmapImage();
+    }
+
+    public override DynamicGridColumns GenerateColumns()
+    {
+        var columns = base.GenerateColumns();
+
+        var newCols = new DynamicGridColumns();
+        foreach(var column in columns)
         {
-            linkcolumn = $"{linkcolumn.Replace(".","_")}_ID";
-            var jobids = DataGrid.SelectedItems.Select(x => (x as DataRowView)!.Row[linkcolumn]).Distinct().ToArray();
-            _jobScopeAction.IsEnabled = jobids.Length == 1;
+            column.Alignment = Alignment.MiddleCenter;
+
+            if (column.ColumnName == "ID" || column.ColumnName == "Form.ID"
+                || column.ColumnName == "Parent.ID" || column.ColumnName == "FormData"
+                || column.ColumnName == "Location.Latitude" || column.ColumnName == "Location.Longitude")
+            {
+                continue;
+            }
+            else if (column.ColumnName == "Number")
+            {
+                column.Width = 80;
+                column.Alignment = Alignment.MiddleCenter;
+            }
+            else if (column.ColumnName == "Description")
+            {
+                column.Width = 250;
+                column.Alignment = Alignment.MiddleLeft;
+            }
+            else if (column.ColumnName == "Location.Timestamp")
+            {
+                continue;
+            }
+            else if (ParentType is not null && DigitalFormsDashboard.ParentColumns.TryGetValue(ParentType, out var pColumns)
+                && pColumns.FirstOrDefault(x => x.Item1 == column.ColumnName)?.Item2 is string header)
+            {
+                column.Caption = header;
+            }
+            else if (column.ColumnName == JobLink + ".JobNumber")
+            {
+                column.Width = 60;
+                column.Caption = "Job No";
+            }
+            else if (column.ColumnName == JobScopeLink + ".Number")
+            {
+                column.Width = 50;
+                column.Caption = "Scope";
+            }
+            else if (column.ColumnName == "Parent.Description")
+            {
+                column.Caption = Categories.FirstOrDefault(x => x.Item2 == FormType)?.Item3 ?? "Parent";
+                column.Alignment = Alignment.MiddleLeft;
+                column.Width = 250;
+            }
+            else if (column.ColumnName == "Completed")
+            {
+                column.Width = 100;
+                column.Format = "dd MMM yy hh:mm";
+            }
+            else if (column.ColumnName == "Completed By")
+            {
+                column.Width = 100;
+            }
+            else if (column.ColumnName == "Processed")
+            {
+                column.Width = 100;
+            }
+            else if (column.ColumnName == "Created By")
+            {
+                column.Width = 100;
+            }
+            else if (column.ColumnName == "Created")
+            {
+                column.Width = 100;
+                column.Format = "dd MMM yy hh:mm";
+            }
+            else if (QuestionCodes.TryGetValue(column.ColumnName, out var questionHeader))
+            {
+                column.Width = 100;
+                column.Caption = questionHeader;
+            }
+            else
+            {
+                continue;
+            }
+            newCols.Add(column);
         }
+        return newCols;
     }
 }