Problem
I started an open source Markdown editor using C#. I’m a huge fan of the MVC pattern, however I am having trouble refactoring my back-end form code. It’s getting pretty long and I was wondering if anyone had tips on which pieces I can move to separate classes.
Here is my main form C# code:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace Marker.UI
{
public partial class MainWindow : Form
{
private String appName;
private MarkdownConverter converter;
private FileHandler fileHandler;
private String lastSavedFilename, lastSavedFilePath;
private bool markdownTextChanged;
protected Font markdownFont, htmlFont;
#region Constructors
public MainWindow(String filePath)
{
InitializeWindow();
OpenFile(filePath);
}
public MainWindow()
{
InitializeWindow();
}
#endregion
private void InitializeWindow()
{
InitializeComponent();
this.Size = UserSettings.LoadWindowSize();
this.Icon = Properties.Resources.Marker;
appName = Application.ProductName;
markdownFont = Properties.Settings.Default.MarkdownFont;
htmlFont = Properties.Settings.Default.HtmlFont;
converter = new MarkdownConverter();
converter.Font = htmlFont;
fileHandler = new FileHandler();
lastSavedFilename = "";
lastSavedFilePath = "";
markdownTextChanged = false;
}
#region Form Events
private void MainWindow_FormClosing(object sender, FormClosingEventArgs e)
{
UserSettings.SaveWindowSize(this.Size);
if (!PromptForUnsavedChanges())
e.Cancel = true;
}
private void markdownTextBox_TextChanged(object sender, EventArgs e)
{
markdownTextChanged = true;
markdownPreview.DocumentText = converter.ToHtml(markdownTextBox.Text);
}
#endregion
#region Menu Events
private void newMenuItem_Click(object sender, EventArgs e)
{
NewMarkdownDocument();
}
private void openMenuItem_Click(object sender, EventArgs e)
{
OpenMarkdownDocument();
}
private void saveMenuItem_Click(object sender, EventArgs e)
{
SaveMarkdownDocument();
}
private void saveAsMenuItem_Click(object sender, EventArgs e)
{
String filePath = SaveMarkdownDialog();
SaveFile(filePath);
markdownTextChanged = false;
}
private void exportToHTMLToolMenuItem_Click(object sender, EventArgs e)
{
String filePath = SaveHtmlDialog();
ExportHtml(filePath);
}
private void exitMenuItem_Click(object sender, EventArgs e)
{
Application.Exit();
}
private void aboutMenuItem_Click(object sender, EventArgs e)
{
ShowAboutWindow();
}
private void preferencesMenuItem_Click(object sender, EventArgs e)
{
ShowPreferenceWindow();
}
#endregion
#region Dialogs
/// <summary>
/// Shows SaveFileDialog with Markdown filter
/// </summary>
/// <returns>filePath - Path to which user selected </returns>
private String SaveMarkdownDialog()
{
SaveFileDialog saveDialog = new SaveFileDialog();
saveDialog.Filter = "Markdown (*.md)|*.md";
saveDialog.ShowDialog();
return saveDialog.FileName;
}
/// <summary>
/// Shows SaveFileDialog with HTML filter
/// </summary>
/// <returns>filePath - Path to file which user selected</returns>
private String SaveHtmlDialog()
{
SaveFileDialog saveDialog = new SaveFileDialog();
saveDialog.Filter = "HTML (*.html)|*html";
saveDialog.DefaultExt = "html";
saveDialog.FileName = "Untitled.html";
saveDialog.ShowDialog();
return saveDialog.FileName;
}
/// <summary>
/// Shows OpenFileDialog with Markdown filter
/// </summary>
/// <returns>filePath - Path to which user selected</returns>
private String OpenMarkdownDialog()
{
OpenFileDialog fileDialog = new OpenFileDialog();
fileDialog.Filter = "Markdown (*.md)|*md";
fileDialog.Title = "Open Markdown file";
fileDialog.RestoreDirectory = true;
fileDialog.ShowDialog();
return fileDialog.FileName;
}
/// <summary>
/// Shows About Box
/// </summary>
private void ShowAboutWindow()
{
using (AboutBox aboutWindow = new AboutBox())
{
aboutWindow.ShowDialog(this);
}
}
private void ShowPreferenceWindow()
{
using (PreferenceWindow preferenceWindow = new PreferenceWindow())
{
preferenceWindow.ShowDialog(this);
markdownFont = preferenceWindow.markdownFont;
htmlFont = preferenceWindow.htmlFont;
}
}
#endregion
/// <summary>
/// Appends last saved filename to Main window's title
/// </summary>
private void RefreshTitle()
{
if (lastSavedFilename.Trim().Length > 0)
this.Text = String.Format("{0} - {1}", lastSavedFilename, appName);
else
this.Text = appName;
}
/// <summary>
/// Setter for the lastSavedFilePath and lastSavedFilename
/// </summary>
private void SetLastSavedFile(String filePath)
{
lastSavedFilePath = filePath;
int startOfFileName = lastSavedFilePath.LastIndexOf("\") + 1;
int length = lastSavedFilePath.Length - startOfFileName;
lastSavedFilename = lastSavedFilePath.Substring(startOfFileName, length);
}
/// <summary>
/// Opens file from filePath and puts it into markdown TextBox
/// </summary>
/// <param name="filePath">File path to Markdown file</param
private void OpenFile(String filePath)
{
markdownTextBox.Text = fileHandler.OpenFile(filePath);
SetLastSavedFile(filePath);
RefreshTitle();
}
/// <summary>
/// Takes Markdown text and sends it to the fileHandler for saving.
/// </summary>
/// <param name="filePath">File path to save the Markdown file</param>
private void SaveFile(String filePath)
{
if (filePath.Trim() == "") return;
fileHandler.MarkdownText = markdownTextBox.Text;
fileHandler.SaveMarkdown(filePath);
SetLastSavedFile(filePath);
RefreshTitle();
}
/// <summary>
/// Takes the HTML from the preview and sends it to the
/// fileHandler for saving.
/// </summary>
/// <param name="filePath">File path to save the HTML file</param>
private void ExportHtml(String filePath)
{
if (filePath.Trim() == "") return;
fileHandler.HtmlText = markdownPreview.DocumentText;
fileHandler.SaveHtml(filePath);
}
private DialogResult PromptUserToSave()
{
String promptMessage =
String.Format("Do you want to save changes to {0}", lastSavedFilePath);
return MessageBox.Show(
promptMessage, "Save Changes", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);
}
private void NewMarkdownDocument()
{
if (PromptForUnsavedChanges())
{
SetLastSavedFile("");
markdownTextChanged = false;
RefreshTitle();
Clear();
}
}
private void SaveMarkdownDocument()
{
String filePath = lastSavedFilePath != "" ? lastSavedFilePath : SaveMarkdownDialog();
SaveFile(filePath);
markdownTextChanged = false;
}
private void OpenMarkdownDocument()
{
if (PromptForUnsavedChanges())
{
String openPath = OpenMarkdownDialog();
OpenFile(openPath);
}
}
/// <summary>
/// Checks if there are any unsaved changes and prompts
/// the user accordingly.
/// </summary>
/// <returns>Returns false if the user presses cancel</returns>
private bool PromptForUnsavedChanges()
{
if (!markdownTextChanged) return true;
DialogResult result = PromptUserToSave();
if (result == DialogResult.Cancel) return false;
if (result == DialogResult.Yes) SaveMarkdownDocument();
return true;
}
/// <summary>
/// Clears the MarkdownTextBox and MarkdownPreview
/// </summary>
private void Clear()
{
markdownTextBox.Text = "";
markdownPreview.DocumentText = "";
}
}
}
The rest of the repository can be found here: https://github.com/chrisledet/Marker
Solution
There is a port to C# of PureMVC out there that I have been using, and made modifications to it to do the following:
- In PureMVC the views are called Mediators, so the technique is to group concerns into a mediator — so your MainForm will have numerous Mediators (basically, ideally, one for every control — with the exception of menus, which are generally broken down into one mediator for every submenu)
- The painful part is hooking up the windows/controls event mechanism to the MVC, you dont want to write a bunch of stubs that call into the Mediator stubs. So you use reflection to do the work at the point where the mediator is instantiated.
- What you end up with is a MainForm.cs that has nothing in it, it is completely managed by its mediators in a code-behind fashion.
- If you’ve done everything correctly, you should be able to take mediators out of your system and nothing evil will happen (ie, you can still compile, and run, but certain things simply won’t work [no crashes]).
If you decide to try out this library, here is a couple tips for changing things up.
In the mediator, there is a callback called ListNotificationInterests, which is called when the mediator is registered. I found this to be an awkward way of showing the notifications so the modification is to use reflection again to make the registerer look for certain dummy members in the mediator, named in a particular fashion —
public const string ___P_APPEARANCECHANGED="MainFmMed_APPEARANCECHANGED"
That shows a published notification, so you would reference that from another file when listing an interest — like so —
private const string ___I_MainFmMed_APPEARANCECHANGED=MainFmMed.___P_APPEARANCECHANGED;
The registerer will pick up on that signature and register you to receive those events. Doing things this way exposes the publish/interest system to your coding tools since it is not buried in a function. That way it is easier to figure out what is going on. (one of the problems with MVC is that you cannot follow what happens simply by following functions around). So, if you have a decent editor, you can even make some macros that will do the proper kind of search when you are on one of these notification identifiers — revealing all who listen and the locations where it is dispatched.
You will notice that doing it the above way with the interest creates a dependency, if the XXX of XXX.__P_MMM is not present, you cant compile (sometimes this is useful) — but to get rid of it you just use the literal:
private const string ___I_MainFmMed_APPEARANCECHANGED="MainFmMed_APPEARANCECHANGED";
The mediators get hooked up like so:
protected override void hookEvents() {
assignFormEvent("Shown");
}
Which hooks the event to:
private void EvtShown(object sender_, EventArgs e_){}
Additionally:
/// // If I am a -Form- mediator - options are...
/// assignFormEvent("Shown","evtShown");
/// assignFormEvent("Shown"[,"EvtShown"]);
/// assignEvent(object c_, string event_name_, string handler_);
/// assignControlEvent("Button1","Click","evtClick");
/// assignControlEvent("Button1","Click"[,"EvtClick"]);
/// assignFormLookAndFeelEvent("evtFormLookAndFeel"); //look change evt
///
/// // If I am a control Mediator - options are...
/// assignEvent("Click","evtClick"); //assign to view component (control I am mediating)
/// assignEvent(button1,"Click","evtClick"); // button1 must be made a property of mediator using reflection
/// assignAllControlEvents();
Controls you are mediating are implemented as properties:
private TextBox buggTB{
get{return GetControl("buggTB") as TextBox;}
}
If mediating a single control, I use this (it) to genericize things:
private PanelControl it {
get{
return ViewComponent as PanelControl;
}
}
I hope that gives you some ideas.