Following on from this question I have added some more functionality to Markdown Markup, and made it more WPF idiomatic.
It now supports saving data from any of the four boxes, and loading Markdown or CSS files.
Everything seems to work, so as always, any tips/pointers/critique is/are welcome.
So, first, the new MainWindow.xaml.cs
:
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
}
private MainWindowViewModel ViewModel => DataContext as MainWindowViewModel;
private void renderPreviewBrowser_Navigating(object sender, NavigatingCancelEventArgs e)
{
// This prevents links in the page from navigating, this also means we cannot call WebBrowser.Navigate for any browsers with this event.
if (e.Uri != null)
{
e.Cancel = true;
}
}
}
Nice and succinct.
The new MainWindow.xaml
:
<Window x:Class="Markdown_Markup.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Markdown_Markup"
mc:Ignorable="d"
Title="MainWindow" Height="539" Width="749"
d:DataContext="{d:DesignInstance local:MainWindowViewModel}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<DockPanel Grid.ColumnSpan="3">
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<MenuItem Header="_Open">
<MenuItem Header="_Markdown" Command="{Binding OpenMarkdownCommand}"/>
<MenuItem Header="_CSS" Command="{Binding OpenCssCommand}"/>
</MenuItem>
<MenuItem Header="_Save">
<MenuItem Header="_Markdown" Command="{Binding SaveMarkdownCommand}" />
<MenuItem Header="_CSS" Command="{Binding SaveCssCommand}"/>
<MenuItem Header="_Generated HTML" Command="{Binding SaveGeneratedHtmlCommand}"/>
<MenuItem Header="_Rendered HTML" Command="{Binding SaveRenderedHtmlCommand}"/>
</MenuItem>
</MenuItem>
</Menu>
<StackPanel></StackPanel>
</DockPanel>
<StatusBar Height="24" VerticalAlignment="Bottom" Grid.ColumnSpan="3" Grid.Row="2"/>
<TextBox Margin="5,45,5,29" TextWrapping="Wrap" Grid.RowSpan="3" Text="{Binding MarkdownContent, UpdateSourceTrigger=PropertyChanged}" AcceptsReturn="True" AcceptsTab="True"/>
<TextBox Margin="5,28,5,5" TextWrapping="Wrap" Grid.Column="1" Grid.Row="1" IsReadOnly="True" Text="{Binding HtmlContent}"/>
<TextBox Margin="5,26,5,29" TextWrapping="Wrap" Grid.Column="1" Grid.Row="2" IsReadOnly="True" Text="{Binding HtmlRenderContent}"/>
<TextBox Margin="5,45,5,0" TextWrapping="Wrap" Grid.Column="1" Text="{Binding CssContent, UpdateSourceTrigger=PropertyChanged}" AcceptsReturn="True" AcceptsTab="True"/>
<WebBrowser local:BrowserBehavior.Html="{Binding HtmlRenderContent}" Grid.Column="2" Margin="5,45,5,29" Grid.RowSpan="3" Navigating="renderPreviewBrowser_Navigating" />
<Label Content="Markdown Content:" HorizontalAlignment="Left" Margin="5,19,0,0" VerticalAlignment="Top"/>
<Label Content="Additional CSS:" Grid.Column="1" HorizontalAlignment="Left" Margin="5,19,0,0" VerticalAlignment="Top"/>
<Label Content="Markdown HTML:" Grid.Column="1" HorizontalAlignment="Left" Margin="5,2,0,0" Grid.Row="1" VerticalAlignment="Top"/>
<Label Content="Render HTML:" Grid.Column="1" HorizontalAlignment="Left" Margin="5,0,0,0" Grid.Row="2" VerticalAlignment="Top"/>
<Label Content="HTML Preview:" Grid.Column="2" HorizontalAlignment="Left" Margin="5,19,0,0" VerticalAlignment="Top"/>
</Grid>
</Window>
This is a bit larger than last time.
The BrowserBehavior.cs
:
/// <summary>
/// Represents a behavior to control WebBrowser binding to an HTML string.
/// </summary>
/// <remarks>
/// Adopted from: http://stackoverflow.com/a/4204350/4564272
/// </remarks>
public class BrowserBehavior
{
public static readonly DependencyProperty HtmlProperty = DependencyProperty.RegisterAttached(
"Html",
typeof(string),
typeof(BrowserBehavior),
new FrameworkPropertyMetadata(OnHtmlChanged));
[AttachedPropertyBrowsableForType(typeof(WebBrowser))]
public static string GetHtml(WebBrowser d) => (string)d.GetValue(HtmlProperty);
public static void SetHtml(WebBrowser d, string value)
{
d.SetValue(HtmlProperty, value);
}
static void OnHtmlChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
var webBrowser = dependencyObject as WebBrowser;
webBrowser?.NavigateToString(e.NewValue as string ?? " ");
}
}
It's slightly modified from the Stack Overflow question mentioned in the XML comment.
A DelegateCommand
(given to me by Mat's Mug, which I made a couple changes to):
public class DelegateCommand : ICommand
{
private readonly Predicate<object> _canExecute;
private readonly Action<object> _execute;
public DelegateCommand(Action<object> execute, Predicate<object> canExecute = null)
{
_canExecute = canExecute;
_execute = execute;
}
public bool CanExecute(object parameter) => _canExecute == null || _canExecute.Invoke(parameter);
public void Execute(object parameter)
{
_execute.Invoke(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
}
Lastly, and this is the more fun of the files, the MainWindowViewModel.cs
:
public class MainWindowViewModel : INotifyPropertyChanged
{
private Markdown _markdown;
private string _markdownContent;
private string _cssContent;
private string _htmlContent;
private string _htmlRenderContent;
public MainWindowViewModel()
{
_markdown = new Markdown();
SaveMarkdownCommand = new DelegateCommand(SaveMarkdown, CanSaveMarkdown);
SaveCssCommand = new DelegateCommand(SaveCss, CanSaveCss);
SaveGeneratedHtmlCommand = new DelegateCommand(SaveGeneratedHtml, CanSaveGeneratedHtml);
SaveRenderedHtmlCommand = new DelegateCommand(SaveRenderedHtml, CanSaveRenderedHtml);
OpenMarkdownCommand = new DelegateCommand(OpenMarkdown, CanOpenMarkdown);
OpenCssCommand = new DelegateCommand(OpenCss, CanOpenCss);
}
public ICommand SaveMarkdownCommand { get; }
public ICommand SaveCssCommand { get; }
public ICommand SaveGeneratedHtmlCommand { get; }
public ICommand SaveRenderedHtmlCommand { get; }
public ICommand OpenMarkdownCommand { get; }
public ICommand OpenCssCommand { get; }
public string MarkdownContent
{
get { return _markdownContent; }
set
{
_markdownContent = value;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(MarkdownContent)));
UpdateHtml();
}
}
public string CssContent
{
get { return _cssContent; }
set
{
_cssContent = value;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(CssContent)));
UpdateHtml();
}
}
public void UpdateHtml()
{
var html = _markdown.Transform(MarkdownContent);
HtmlContent = html;
html = $"<html>\r\n\t<head>\r\n\t\t<style>\r\n\t\t\t{CssContent}\r\n\t\t</style>\r\n\t</head>\r\n\t<body>\r\n\t\t{html}\r\n\t</body>\r\n</html>";
HtmlRenderContent = html;
}
public string HtmlContent
{
get { return _htmlContent; }
set
{
_htmlContent = value;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(HtmlContent)));
}
}
public string HtmlRenderContent
{
get { return _htmlRenderContent; }
set
{
_htmlRenderContent = value;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(HtmlRenderContent)));
}
}
public bool CanSaveMarkdown(object parameter) => !string.IsNullOrWhiteSpace(MarkdownContent);
public void SaveMarkdown(object parameter)
{
var dialog = new SaveFileDialog();
dialog.AddExtension = true;
dialog.Filter = "Markdown Files|*.md|All Files|*.*";
var result = dialog.ShowDialog();
if (result.Value)
{
using (var sw = new StreamWriter(dialog.FileName))
{
sw.WriteLine(MarkdownContent);
}
}
}
public bool CanSaveCss(object parameter) => !string.IsNullOrWhiteSpace(CssContent);
public void SaveCss(object parameter)
{
var dialog = new SaveFileDialog();
dialog.AddExtension = true;
dialog.Filter = "CSS Files|*.css|All Files|*.*";
var result = dialog.ShowDialog();
if (result.Value)
{
using (var sw = new StreamWriter(dialog.FileName))
{
sw.WriteLine(CssContent);
}
}
}
public bool CanSaveGeneratedHtml(object parameter) => !string.IsNullOrWhiteSpace(HtmlContent);
public void SaveGeneratedHtml(object parameter)
{
var dialog = new SaveFileDialog();
dialog.AddExtension = true;
dialog.Filter = "HTML Files|*.html|All Files|*.*";
var result = dialog.ShowDialog();
if (result.Value)
{
using (var sw = new StreamWriter(dialog.FileName))
{
sw.WriteLine(HtmlContent);
}
}
}
public bool CanSaveRenderedHtml(object parameter) => !string.IsNullOrWhiteSpace(HtmlRenderContent);
public void SaveRenderedHtml(object parameter)
{
var dialog = new SaveFileDialog();
dialog.AddExtension = true;
dialog.Filter = "HTML Files|*.html|All Files|*.*";
var result = dialog.ShowDialog();
if (result.Value)
{
using (var sw = new StreamWriter(dialog.FileName))
{
sw.WriteLine(HtmlRenderContent);
}
}
}
public bool CanOpenMarkdown(object parameter) => true;
public void OpenMarkdown(object parameter)
{
var dialog = new OpenFileDialog();
dialog.AddExtension = true;
dialog.Filter = "Markdown Files|*.md|All Files|*.*";
var result = dialog.ShowDialog();
if (result.Value)
{
using (var sr = new StreamReader(dialog.FileName))
{
MarkdownContent = sr.ReadToEnd();
}
}
}
public bool CanOpenCss(object parameter) => true;
public void OpenCss(object parameter)
{
var dialog = new OpenFileDialog();
dialog.AddExtension = true;
dialog.Filter = "CSS Files|*.css|All Files|*.*";
var result = dialog.ShowDialog();
if (result.Value)
{
using (var sr = new StreamReader(dialog.FileName))
{
CssContent = sr.ReadToEnd();
}
}
}
public void OnPropertyChanged(PropertyChangedEventArgs e)
{
var handler = PropertyChanged;
handler?.Invoke(this, e);
}
public event PropertyChangedEventHandler PropertyChanged;
}
All comments and critique are welcome.
It's also on GitHub now.
CanExecute(object parameter)
method seems wrong. Superficially, I was expecting_canExecute != null && _canExecute.Invoke(parameter);
. \$\endgroup\$Predicate<object>
was specified for_canExecute
, then the command can always execute. \$\endgroup\$