This is another review on a program I've asked about before, now translated from VBA into C#. I'm sure I've brought over a lot of bad habits with me, so I'm spotlighting some key areas I'd love open-ended feedback on.
This is a WPF desktop app.
Program Function
As before, this program makes emergency changes to medical claim files, delivers all the corrected files analysts need, and produces a changelog as well for their review.
Main method - Fix names changed to protect the innocent
class FixTextFiles
{
[System.STAThreadAttribute()]
//[System.Diagnostics.DebuggerNonUserCodeAttribute()]
//[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "4.0.0.0")]
static void Main()
{
var model = new Model();
ActiveFixes activeFixes = new ActiveFixes();
ActiveReports activeReports = new ActiveReports();
FileInfo[] files = null;
if (!ModelIsSetUpBasedOnArgumentsOrUI(model, activeFixes, activeReports, ref files))
{
return;
}
var DoubleProgressBar = new ViewPlusViewModel.DoubleProgressBar();
DoubleProgressBar.ProgressBarFilesCompleted.Maximum = files.GetUpperBound(0) + 1;
DoubleProgressBar.Show();
DoubleProgressBar.Activate();
for (var i = files.GetLowerBound(0); i <= files.GetUpperBound(0); i++)
{
DoubleProgressBar.ProgressBarFilesCompleted.Value = i;
DoubleProgressBar.LabelFilesCompleted.Content = "Files Completed: " + i + " / " + DoubleProgressBar.ProgressBarFilesCompleted.Maximum + ", " + files[i].Name;
string entireFile = File.ReadAllText(files[i].FullName);
if (entireFile != string.Empty)
{
var originalFileLines = entireFile.Split(new string[] { model.Delimiter1 }, StringSplitOptions.None);
var revisedFileLines = entireFile.Split(new string[] { model.Delimiter1 }, StringSplitOptions.None);
activeFixes.ResetIndicatorsOnNewFile();
activeReports.ResetIndicatorsOnNewFile();
DoubleProgressBar.ProgressBarLinesCompleted.Maximum = originalFileLines.GetUpperBound(0) + 1;
// --- Begin Manipulation ---
for (var currentLineNumber = originalFileLines.GetLowerBound(0); currentLineNumber <= originalFileLines.GetUpperBound(0); currentLineNumber++)
{
DoubleProgressBar.ProgressBarLinesCompleted.Value = currentLineNumber;
DoubleProgressBar.LabelLinesCompleted.Content = "Lines Completed: " + currentLineNumber + " / " + DoubleProgressBar.ProgressBarLinesCompleted.Maximum;
// --- Ongoing variables ---
var currentLine = originalFileLines[currentLineNumber];
var segmentType = currentLine.Substring(0, currentLine.IndexOf(model.Delimiter3));
// --- Fixes ---
if (activeFixes.FixClassThatFixesIndividualLines1 != null)
{
// check things and do FixClass1 methods
}
if (activeFixes.FixClassThatFixesIndividualLines2 != null)
{
// check things and do FixClass2 methods
}
// repeat for 40+ fix/report classes
}
DoubleProgressBar.ProgressBarLinesCompleted.Value = DoubleProgressBar.ProgressBarLinesCompleted.Maximum;
DoubleProgressBar.ProgressBarFilesCompleted.Value = i + 1;
if (activeReports.Count == 0)
{
if (model.CreateChangelogSheets) { WriteOriginalAndUpdatedSegmentsToCSVAndNoteDifferences(originalFileLines, revisedFileLines, files[i]); }
WriteUpdatedSegmentsToNewFile(revisedFileLines, model.Delimiter1, files[i], model.FixedFilesDestination);
}
if (activeReports.ReportClassThatReportsOnWholeFiles1 != null)
{
// check things and do ReportClass1 methods
}
if (activeReports.ReportClassThatReportsOnWholeFiles2 != null)
{
// check things and do ReportClass2 methods
}
}
}
activeFixes.ReprotectSettingsSheets();
activeReports.ReprotectSettingsSheets();
var tempDirectoryPath = Path.Combine(new string[] { Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "(Program Name)" });
if (Directory.Exists(tempDirectoryPath))
{
try
{
Directory.Delete(tempDirectoryPath, true);
}
catch (IOException e)
{
}
}
UpdateTelemetryFile(files[0].DirectoryName, files.GetUpperBound(0) + 1, activeFixes, activeReports);
if (model.LaunchedFromBatchFile == false) { MessageBox.Show("All files have been fixed and saved to the same folders as the source files with \"Fixed\" added to their filenames. No original files have been modified."); }
}
This is mostly a console app with some light UI tacked on for easy user configuration - start getting ready for user config
private static bool ModelIsSetUpBasedOnArgumentsOrUI(Model model, ActiveFixes activeFixes, ActiveReports activeReports, ref FileInfo[] files)
{
var commandLineArgs = Environment.GetCommandLineArgs();
switch (commandLineArgs.GetUpperBound(0))
{
case 0:
if (!UserPromptedSettingsWereWrittenToModel(ref model, ref activeFixes, ref activeReports))
{
return false;
}
files = GetFilesToWorkWith();
if (files.Length == 0)
{
return false;
}
else
{
return true;
}
case 7:
// Model is setup based on command line arguments instead
}
}
This is the major UI portion - user goes through a sequence of two main config windows (and potentially one child window) before selecting their files with GetFilesToWorkWith()
public static bool UserPromptedSettingsWereWrittenToModel(ref Model model, ref ActiveFixes activeFixes, ref ActiveReports activeReports)
{
var viewModel = new ViewModel();
viewModel.Setup();
var tempDirectoryPath = Path.Combine(new string[] { Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "(Program name)", "Model Data" });
if (!Directory.Exists(tempDirectoryPath))
{
Directory.CreateDirectory(tempDirectoryPath);
}
var tempFilePath = Path.Combine(new string[] { tempDirectoryPath, "Fixes Reports List.csv" });
File.WriteAllText(tempFilePath, Properties.Resources.Fixes_Reports_List);
viewModel.FixesReportsTable = GeneralTools.GetDataTableFromCSV(tempFilePath, "|", true, false);
var parseSettings = new ViewPlusViewModel.ParseSettings();
parseSettings.InitializeComponent();
var fixSelector = new ViewPlusViewModel.FixSelector(viewModel);
fixSelector.InitializeComponent();
var seeAllFixesReports = new ViewPlusViewModel.SeeAllFixesReports(viewModel);
seeAllFixesReports.InitializeComponent();
parseSettings.ShowDialog();
var nextWindowToOpen = "TBD";
while (!string.IsNullOrEmpty(nextWindowToOpen) && !nextWindowToOpen.Equals("Select Text Files"))
{
switch (GetNextWindowToOpen(parseSettings, fixSelector, seeAllFixesReports))
{
case "Parse Settings":
parseSettings.ShowDialog();
break;
case "Fix Selector":
fixSelector.ShowDialog();
break;
case "See All Fixes And Reports":
if (fixSelector.FixesOrReports.Equals("Fixes"))
{
seeAllFixesReports.UpdateTableType("Fix");
}
if (fixSelector.FixesOrReports.Equals("Reports"))
{
seeAllFixesReports.UpdateTableType("Report");
}
seeAllFixesReports.ShowDialog();
break;
case "Select Text Files":
nextWindowToOpen = "Select Text Files";
break;
case null:
parseSettings.Close();
if (fixSelector.IsLoaded) fixSelector.Close();
if (seeAllFixesReports.IsLoaded) { seeAllFixesReports.Close(); }
return false;
}
}
if (fixSelector.FixesOrReports.Equals("Fixes"))
{
activeFixes.Setup(fixSelector.ActiveFixes, viewModel.FixesReportsTable);
}
if (fixSelector.FixesOrReports.Equals("Reports"))
{
activeReports.Setup(fixSelector.ActiveReports, viewModel.FixesReportsTable);
}
model.Setup(parseSettings.Delimiter1, parseSettings.Delimiter2, parseSettings.Delimiter3, parseSettings.Delimiter4, Convert.ToBoolean(parseSettings.CreateChangelogSheets));
parseSettings.Close();
fixSelector.Close();
if (seeAllFixesReports.IsLoaded) { seeAllFixesReports.Close(); }
return true;
}
Since I'm new
ing and ShowDialog
ing UIwindows from the code rather than Main()
ing in a window in a more pure WPF fashion, I use code to determine where to go next
private static string GetNextWindowToOpen(ViewPlusViewModel.ParseSettings parseSettings, ViewPlusViewModel.FixSelector fixSelector, ViewPlusViewModel.SeeAllFixesReports seeAllFixesReports)
{
if (fixSelector.GoBack)
{
fixSelector.GoBack = false;
return "Parse Settings";
}
if (parseSettings.GoToNextWindow ||
seeAllFixesReports.GoToNextWindow ||
seeAllFixesReports.GoBack)
{
parseSettings.GoToNextWindow = false;
seeAllFixesReports.GoToNextWindow = false;
seeAllFixesReports.GoBack = false;
return "Fix Selector";
}
if (fixSelector.GoToChildWindow)
{
fixSelector.GoToChildWindow = false;
return "See All Fixes And Reports";
}
if (fixSelector.GoToNextWindow)
{
fixSelector.GoToNextWindow = false;
return "Select Text Files";
}
return null;
}
The first UI window, ParseSettings - users put in 5 values for the Model. Delimiter names changed to protect the innocent.
public partial class ParseSettings : Window
{
public static readonly DependencyProperty Delimiter1Property = DependencyProperty.Register("Delimiter1", typeof(string), typeof(ParseSettings), new PropertyMetadata("(Default A)"));
public static readonly DependencyProperty Delimiter2Property = DependencyProperty.Register("Delimiter2", typeof(string), typeof(ParseSettings));
public static readonly DependencyProperty Delimiter3Property = DependencyProperty.Register("Delimiter3", typeof(string), typeof(ParseSettings), new PropertyMetadata("(Default B)"));
public static readonly DependencyProperty Delimiter4Property = DependencyProperty.Register("Delimiter4", typeof(string), typeof(ParseSettings), new PropertyMetadata("(Default C)"));
public static readonly DependencyProperty CreateChangelogSheetsProperty = DependencyProperty.Register("CreateChangelogSheets", typeof(string), typeof(ParseSettings), new PropertyMetadata("True"));
public string Delimiter1
{
get { return GetValue(Delimiter1Property) as string; }
set { SetValue(Delimiter1Property, value); }
}
public string Delimiter2
{
get { return GetValue(Delimiter2Property) as string; }
set { SetValue(Delimiter2Property, value); }
}
public string Delimiter3
{
get { return GetValue(Delimiter3Property) as string; }
set { SetValue(Delimiter3Property, value); }
}
public string Delimiter4
{
get { return GetValue(Delimiter4Property) as string; }
set { SetValue(Delimiter4Property, value); }
}
public string CreateChangelogSheets
{
get { return GetValue(CreateChangelogSheetsProperty) as string; }
set { SetValue(CreateChangelogSheetsProperty, value); }
}
public bool GoToNextWindow { get; set; }
public ParseSettings()
{
InitializeComponent();
GoToNextWindow = false;
}
protected void MakeChangelogSheetsButton_Clicked(object sender, RoutedEventArgs e)
{
var senderAsButton = sender as Button;
if (senderAsButton == ButtonCreateChangelogSheets)
{
switch (this.CreateChangelogSheets)
{
case "True":
this.CreateChangelogSheets = "False";
senderAsButton.Background = Brushes.Pink;
break;
case "False":
this.CreateChangelogSheets = "True";
senderAsButton.Background = Brushes.PaleGreen;
break;
}
}
}
private void SeeIfGoToNextWindowButtonCanBeEnabled(object sender, TextChangedEventArgs e)
{
if (Validation.GetHasError(this.InputDelimiter1) ||
Validation.GetHasError(this.InputDelimiter2) ||
Validation.GetHasError(this.InputDelimiter3) ||
Validation.GetHasError(this.InputDelimiter4))
{
this.ButtonSelectFixesOrReports.IsEnabled = false;
}
else
{
this.ButtonSelectFixesOrReports.IsEnabled = true;
}
}
private void GoToNextWindow_Click(object sender, RoutedEventArgs e)
{
if (sender == this.ButtonSelectFixesOrReports)
{
this.GoToNextWindow = true;
}
this.Hide();
}
}
The second UI window, FixSelector - users can search for desired fixes or pick from a telemetry file-populated list of the most popular. Each fix listed in either search results or the popular options is a button in a ListView
that can be clicked to activate the fix, change the button's color visually, and add or remove it from appropriate ListView
-populating dictionaries
public partial class FixSelector : Window
{
public static readonly DependencyProperty FixesOrReportsProperty = DependencyProperty.Register("FixesOrReports", typeof(string), typeof(FixSelector), new PropertyMetadata("Fixes"));
public static readonly DependencyProperty SegmentFixNameSearchProperty = DependencyProperty.Register("SegmentFixNameSearch", typeof(string), typeof(FixSelector));
public string FixesOrReports
{
get { return GetValue(FixesOrReportsProperty) as string; }
set { SetValue(FixesOrReportsProperty, value); }
}
public string SegmentFixNameSearch
{
get { return GetValue(SegmentFixNameSearchProperty) as string; }
set { SetValue(SegmentFixNameSearchProperty, value); }
}
public bool GoToNextWindow { get; set; }
public bool GoToChildWindow { get; set; }
public bool GoBack { get; set; }
public List<(string FixOrReportName, long CountInTelemetryFile)> MostPopularFixes { get; set; }
public List<(string FixOrReportName, long CountInTelemetryFile)> MostPopularReports { get; set; }
public DataTable CompleteFixesReportsTable { get; set; }
public DataTable CompatibilityTable { get; set; }
public Dictionary<string, string> ActiveFixes { get; set; }
public Dictionary<string, string> ActiveReports { get; set; }
public Dictionary<Button, Button> ClickedButtonsByAddedButtonsInSelectedFixes { get; set; }
public Dictionary<Button, Button> ClickedButtonsByAddedButtonsInSelectedReports { get; set; }
public Dictionary<Button, Button> AddedButtonsInSelectedFixesByClickedButtons { get; set; }
public Dictionary<Button, Button> AddedButtonsInSelectedReportsByClickedButtons { get; set; }
public FixSelector(ViewModel viewModel)
{
InitializeComponent();
GoToNextWindow = false;
GoToChildWindow = false;
GoBack = false;
MostPopularFixes = viewModel.MostPopularFixes;
MostPopularReports = viewModel.MostPopularReports;
PopulateMostPopularOptionsListView();
CompleteFixesReportsTable = viewModel.FixesReportsTable;
CompatibilityTable = viewModel.CompatibilityTable;
ActiveFixes = new Dictionary<string, string>();
ActiveReports = new Dictionary<string, string>();
ClickedButtonsByAddedButtonsInSelectedFixes = new Dictionary<Button, Button>();
ClickedButtonsByAddedButtonsInSelectedReports = new Dictionary<Button, Button>();
AddedButtonsInSelectedFixesByClickedButtons = new Dictionary<Button, Button>();
AddedButtonsInSelectedReportsByClickedButtons = new Dictionary<Button, Button>();
}
private void PopulateMostPopularOptionsListView()
{
this.MostPopularOptions.Items.Clear();
if (this.FixesOrReports.Equals("Fixes"))
{
if (!(this.MostPopularFixes is null))
{
for (var i = 0; i < this.MostPopularFixes.Count; i++)
{
var button = new Button
{
Content = this.MostPopularFixes[i].FixOrReportName
};
button.Click += new RoutedEventHandler(this.IndividualFixReportButton_Click);
this.MostPopularOptions.Items.Add(button);
}
}
}
if (this.FixesOrReports.Equals("Reports"))
{
if (!(this.MostPopularReports is null))
{
for (var i = 0; i < this.MostPopularReports.Count; i++)
{
var button = new Button
{
Content = this.MostPopularReports[i].FixOrReportName
};
button.Click += new RoutedEventHandler(this.IndividualFixReportButton_Click);
this.MostPopularOptions.Items.Add(button);
}
}
}
}
protected void FixesOrReportsButton_Clicked(object sender, RoutedEventArgs e)
{
var senderAsButton = sender as Button;
if (senderAsButton == ButtonFixesOrReports)
{
switch (this.FixesOrReports)
{
case "Fixes":
this.FixesOrReports = "Reports";
senderAsButton.Background = Brushes.LightBlue;
PopulateMostPopularOptionsListView();
this.SelectedFixes.Visibility = Visibility.Hidden;
this.SelectedReports.Visibility = Visibility.Visible;
if (this.ActiveReports.Count == 0)
{
this.ButtonSelectTextFiles.IsEnabled = false;
}
break;
case "Reports":
this.FixesOrReports = "Fixes";
senderAsButton.Background = Brushes.Orange;
PopulateMostPopularOptionsListView();
this.SelectedFixes.Visibility = Visibility.Visible;
this.SelectedReports.Visibility = Visibility.Hidden;
this.ButtonSelectTextFiles.IsEnabled = true;
break;
}
PopulateSearchResults(this.SearchResults, e as TextChangedEventArgs);
}
}
protected void PopulateSearchResults(object sender, TextChangedEventArgs e)
{
this.SearchResults.Items.Clear();
if (string.IsNullOrEmpty(this.SegmentFixNameSearch))
{
return;
}
string typeToReturn;
if (this.FixesOrReports.Equals("Fixes"))
{
typeToReturn = "Fix";
}
else
{
typeToReturn = "Report";
}
var nameFilter = GetCompleteFixesReportsTableRowFilterNameExpression();
this.CompleteFixesReportsTable.DefaultView.RowFilter =
nameFilter + " AND FixOrReport = '" + typeToReturn + "'";
var matchingFixesTable = this.CompleteFixesReportsTable.DefaultView.ToTable();
var matchingFixesList = new List<(string Name, string Description)>();
for (var i = 0; i < matchingFixesTable.Rows.Count; i++)
{
matchingFixesList.Add((Name: matchingFixesTable.Rows[i]["Name"].ToString(), Description: matchingFixesTable.Rows[i]["Description"].ToString()));
}
for (var i = 0; i < matchingFixesList.Count; i++)
{
var button = new Button
{
Content = matchingFixesList[i].Name,
ToolTip = matchingFixesList[i].Description
};
button.Click += new RoutedEventHandler(this.IndividualFixReportButton_Click);
if (typeToReturn.Equals("Fix"))
{
if (this.ActiveFixes.ContainsKey(button.Content.ToString()))
{
button.Background = Brushes.PaleGreen;
}
}
if (typeToReturn.Equals("Report"))
{
if (this.ActiveReports.ContainsKey(button.Content.ToString()))
{
button.Background = Brushes.PaleGreen;
}
}
this.SearchResults.Items.Add(button);
}
}
private void IndividualFixReportButton_Click(object sender, RoutedEventArgs e)
{
var senderAsButton = sender as Button;
if (senderAsButton.Background == Brushes.PaleGreen)
{
senderAsButton.Background = (Brush)new BrushConverter().ConvertFrom("#FFDDDDDD");
RemoveFixOrReportFromAppropriateDictionary(senderAsButton);
}
else
{
senderAsButton.Background = Brushes.PaleGreen;
AddFixOrReportToAppropriateDictionary(senderAsButton);
}
}
private void GoToNextWindow_Click(object sender, RoutedEventArgs e)
{
if (sender == ButtonSelectTextFiles)
{
this.GoToNextWindow = true;
}
if (sender == ButtonBackToParseSettings)
{
this.GoBack = true;
}
if (sender == ButtonSeeAllFixesReports)
{
this.GoToChildWindow = true;
}
this.Hide();
}
}
Last major section: Based upon the dictionary of selected fixes or reports, here's how those ActiveFixes/ActiveReports
classes get populated
class ActiveFixes
{
public long Count { get; set; }
public DataTable FixesReportsTable { get; set; }
public Fixes.FixClass1 FixClass1 { get; set; }
public Fixes.FixClass2 FixClass2 { get; set; }
// etc. for 40+ fixes
public ActiveFixes()
{
Count = 0;
}
public void Setup(Dictionary<string, string> selectionsFromFixSelector, DataTable fixesReportsTable)
{
this.FixesReportsTable = fixesReportsTable;
InstantiateSelectionsAsProperties(selectionsFromFixSelector);
var properties = this.GetType().GetProperties();
for (var i = properties.GetLowerBound(0); i <= properties.GetUpperBound(0); i++)
{
if (!properties[i].Name.Equals("Count") && !properties[i].Name.Equals("FixesReportsTable"))
{
if (properties[i].GetValue(this) != null)
{
this.Count += 1;
var activeFix = properties[i].GetValue(this) as IFixReport;
var fixName = ConversionMethods.GetFixReportNameFromIFixReportName(properties[i].Name);
var fixRows = this.FixesReportsTable.Select("Name = '" + fixName + "'");
activeFix.Settings = _(ProgramName)Tools.GetSettingsSheet(fixRows[0]["SettingsSheet"].ToString());
if (activeFix.Settings != null) {
activeFix.Settings.Unprotect();
activeFix.ConfirmAndSpeedUpSettingsSheet();
}
activeFix.Setup();
}
}
}
}
public void InstantiateSelectionsAsProperties(Dictionary<string, string> selectionsFromFixSelector)
{
foreach (var key in selectionsFromFixSelector.Keys)
{
switch (key)
{
case "Fix Class 1 Full Name":
this.FixClass1 = new Fixes.FixClass1();
break;
case "Fix Class 2 Full Name":
this.FixClass2 = new Fixes.FixClass2();
break;
// etc.
}
}
}