Got it working like this for anyone curious:
MainWindow.xaml.cs
namespace Test
{
public class MyCanvas : Canvas
{
public bool IsGridVisible = false;
#region Dependency Properties
public static DependencyProperty ZoomValueProperty = DependencyProperty.Register("ZoomValue", typeof(double), typeof(MyCanvas), new FrameworkPropertyMetadata(1d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnZoomValueChanged));
public double ZoomValue
{
get
{
return (double)GetValue(ZoomValueProperty);
}
set
{
SetValue(ZoomValueProperty, value);
}
}
private static void OnZoomValueChanged(DependencyObject Object, DependencyPropertyChangedEventArgs e)
{
}
#endregion
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
base.OnRender(dc);
IsGridVisible = ZoomValue > 4.75 ? true : false;
if (IsGridVisible)
{
Pen pen = new Pen(Brushes.Black, 1 / ZoomValue);
pen.DashStyle = DashStyles.Solid;
for (double x = 0; x < this.ActualWidth; x += 1)
{
dc.DrawLine(pen, new Point(x, 0), new Point(x, this.ActualHeight));
}
for (double y = 0; y < this.ActualHeight; y += 1)
{
dc.DrawLine(pen, new Point(0, y), new Point(this.ActualWidth, y));
}
}
}
public MyCanvas()
{
DefaultStyleKey = typeof(MyCanvas);
}
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private WriteableBitmap bitmap = new WriteableBitmap(500, 500, 96d, 96d, PixelFormats.Bgr24, null);
private void Button_Click(object sender, RoutedEventArgs e)
{
int size = 1;
Random rnd = new Random(DateTime.Now.Millisecond);
bitmap.Lock();
for (int y = 0; y < 500; y++)
{
for (int x = 0; x < 500; x++)
{
byte colR = (byte)rnd.Next(256);
byte colG = (byte)rnd.Next(256);
byte colB = (byte)rnd.Next(256);
DrawRectangle(bitmap, size * x, size * y, size, size, Color.FromRgb(colR, colG, colB));
}
}
bitmap.Unlock();
Image.Source = bitmap;
}
public void DrawRectangle(WriteableBitmap writeableBitmap, int left, int top, int width, int height, Color color)
{
int colorData = color.R << 16;
colorData |= color.G << 8;
colorData |= color.B << 0;
int bpp = writeableBitmap.Format.BitsPerPixel / 8;
unsafe
{
for (int y = 0; y < height; y++)
{
int pBackBuffer = (int)writeableBitmap.BackBuffer;
pBackBuffer += (top + y) * writeableBitmap.BackBufferStride;
pBackBuffer += left * bpp;
for (int x = 0; x < width; x++)
{
*((int*)pBackBuffer) = colorData;
pBackBuffer += bpp;
}
}
}
writeableBitmap.AddDirtyRect(new Int32Rect(left, top, width, height));
}
}
}
MainWindow.xaml
<Window x:Class="Test.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:Test"
mc:Ignorable="d"
Title="MainWindow"
Height="Auto"
Width="Auto"
WindowStartupLocation="CenterScreen"
WindowState="Maximized">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="30"/>
</Grid.RowDefinitions>
<ScrollViewer>
<local:MyCanvas ZoomValue="{Binding ElementName=ScaleTransform, Path=ScaleX}">
<local:MyCanvas.LayoutTransform>
<ScaleTransform x:Name="ScaleTransform" ScaleX="{Binding ElementName=Slider, Path=Value}" ScaleY="{Binding ElementName=Slider, Path=Value}"/>
</local:MyCanvas.LayoutTransform>
<Image Canvas.Top="1" Canvas.Left="1" x:Name="Image" RenderOptions.BitmapScalingMode="NearestNeighbor"/>
</local:MyCanvas>
</ScrollViewer>
<StackPanel Grid.Row="1" Orientation="Horizontal">
<Slider x:Name="Slider" Maximum="100" Minimum="0.5" Value="1" Width="200"/>
<Button Click="Button_Click" Content="Click Me!"/>
</StackPanel>
</Grid>
</Window>
We generate a bitmap with random colored pixels and then render the grid lines only if zoomed up close. Performance-wise, this is actually better than expected. I should note, though, that if you attempt to zoom below 50%, the app crashes. Not sure if it's an issue with the grid lines being drawn at a minute size (IsGridVisible = true where ZoomValue < 0.5) or with the bitmap being generated. Either way, cheers!
Update
Didn't realize the grid lines are still behind the contents of the canvas. Haven't worked out a solution for that yet...
Update 2
Replace:
<local:MyCanvas ZoomValue="{Binding ElementName=ScaleTransform, Path=ScaleX}">
<local:MyCanvas.LayoutTransform>
<ScaleTransform x:Name="ScaleTransform" ScaleX="{Binding ElementName=Slider, Path=Value}" ScaleY="{Binding ElementName=Slider, Path=Value}"/>
</local:MyCanvas.LayoutTransform>
<Image Canvas.Top="1" Canvas.Left="1" x:Name="Image" RenderOptions.BitmapScalingMode="NearestNeighbor"/>
</local:MyCanvas>
With:
<Grid>
<Canvas>
<Canvas.LayoutTransform>
<ScaleTransform ScaleX="{Binding ElementName=Slider, Path=Value}" ScaleY="{Binding ElementName=Slider, Path=Value}"/>
</Canvas.LayoutTransform>
<Image Canvas.Top="5" Canvas.Left="5" x:Name="Image" RenderOptions.BitmapScalingMode="NearestNeighbor"/>
</Canvas>
<local:MyGrid ZoomValue="{Binding ElementName=ScaleTransform, Path=ScaleX}">
<local:MyGrid.LayoutTransform>
<ScaleTransform x:Name="ScaleTransform" ScaleX="{Binding ElementName=Slider, Path=Value}" ScaleY="{Binding ElementName=Slider, Path=Value}"/>
</local:MyGrid.LayoutTransform>
</local:MyGrid>
</Grid>
I believe another boost in performance as we are utilizing a simpler control to display the grid lines, plus, the grid lines can be placed either below or above desired controls.
Update 3
I have decided to post my latest solution, which is significantly more efficient and can all be done in XAML:
<Grid>
<Grid.Background>
<DrawingBrush Viewport="0,0,5,5" ViewportUnits="Absolute" TileMode="Tile">
<DrawingBrush.Drawing>
<DrawingGroup>
<DrawingGroup.Children>
<GeometryDrawing Geometry="M-.5,0 L50,0 M0,10 L50,10 M0,20 L50,20 M0,30 L50,30 M0,40 L50,40 M0,0 L0,50 M10,0 L10,50 M20,0 L20,50 M30,0 L30,50 M40,0 L40,50">
<GeometryDrawing.Pen>
<Pen Thickness="1" Brush="Black" />
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingGroup.Children>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Grid.Background>
</Grid>