Reputation: 43
I am a beginner WPF developer. As a learning exercise I am attempting to recreate Visual Studio's Memory window using WPF with the MVVM pattern. For those of you unfamiliar with this window.
For simplicity we assume the memory is readonly and will not change. As per the MVVM pattern we define the following entities:
The Model is a simple object that contains 2 properties:
long Address
byte[] Data
The Model represents a sequence of bytes (Data
) at a defined memory address (Address
).
The ViewModel takes the Model as input and exposes the following properties:
string Addresses
A string
representing the addresses of the bytes displayed on the same row. Referencing the image in the Background section, the Addresses
string
contains the contents of the first column, that is:
string Addresses = "0x022699B0" + '\n' +
"0x022699C8" + '\n' +
"0x022699E0" + '\n' +
...
string HexBytes
A hexadecimal representation of the Data
in the Model. Referencing the image in the Background section, the HexBytes
string
contains the contents of the second column, that is:
string HexBytes = "c0 ac 45 68 06 ..."
string ASCIIBytes
An ASCII representation of the Data
in the Model. Referencing the image in the Background section, the ASCIIBytes
string
contains the contents of the third column, that is:
string ASCIIBytes = "A.Ch....9.1.1.1.2.5 ..."
To summarize, the ViewModel exposes the data given my the Model in a View friendly format.
The View is probably easiest to describe via an XAML code snippet:
<UserControl x:Class="HexEditor.HexView"
xmlns ="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:z="clr-namespace:HexEditor">
<UserControl.DataContext>
<z:MemoryViewModel/>
</UserControl.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="3*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" IsReadOnly="True" Cursor="Arrow" FontFamily="Consolas" Text="{Binding Addresses, Mode=OneWay}"/>
<TextBox Grid.Column="1" IsReadOnly="True" TextWrapping="Wrap" Cursor="Arrow" FontFamily="Consolas" Text="{Binding HexBytes, Mode=OneWay}" />
<TextBox Grid.Column="2" IsReadOnly="True" TextWrapping="Wrap" Cursor="Arrow" FontFamily="Consolas" Text="{Binding ASCIIBytes, Mode=OneWay}" />
</Grid>
Notice the TextWrapping
attribute has been set on the second and third columns. This allows us to display a variable number of bytes per row, depending on the size of the Window.
The problem I am encountering is that the properties the ViewModel must expose depend on the size of the View. In the MVVM pattern, the ViewModel should be independent of the View. This is not a problem for HexBytes
and ASCIIBytes
as these are generated from the Model. The problem is how to generate the Addresses
? To generate this string
I must know the number of characters that will be displayed on each line (so that I can calculate the correct address of the first byte of the next line). This is unfortunately something only the View knows about, as it depends on the size of the View.
My attempt to solve this problem is to capture the SizeChanged
events in the View code-behind and use the TextBox.GetLineLength
method to inform the ViewModel of how many bytes can fit on a line. The ViewModel can then consume this value to generate the correct Addresses
property.
I have two questions regarding my proposed solution:
TextBox
?Thanks so much for any input or comments.
Upvotes: 4
Views: 738
Reputation: 102783
Is there a more elegant way of informing the ViewModel on the number of characters that can be displayed on 1 line
I think your approach is the best one. The outcome you have in mind -- variable column width with inferred column headers -- implies a dependency of the view-model on the view. (Some MVVM approaches use some kind of "messaging" framework (eg, event aggregators) to communicate between view and view-model, but that is just a more formalized way of having the view directly invoke a method on the view-model.) Also, you will need to be handling the SizeChanged
event in any case, in order to re-generate the column headers on each re-size.
Am I using the correct Controls and the correct approach more importantly to get to my end result
It seems fine. You might consider using an ObservableCollection<string>
along with an ItemsControl
or ListBox
(instead of the flat string with line breaks). That would allow you more control over the UI, enabling things like highlighting/selection of individual addresses; synchronized scrolling of the addresses/bytes; and virtualized panels to handle larger data sets without the UI grinding down.
Upvotes: 0
Reputation: 12119
When you capture that value in your event handler just update the VM property that keeps this value from within that event handler and that way VM will always have that value as soon as the event fires...
Since your VM is already the DataContext
of your view this is not breaking the MVVM pattern and updating that value of the VM property is very simple:
This is all in your View code-behind:
Field:
private MyViewModel vm;
In view constructor (or DataContextChanged event if you assign DataContext dynamically):
vm = DataContext as MyViewModel; //Get VM instance from view's DataContext
In event handler:
vm.AddressLenght = TextBox.GetLineLength;
Upvotes: 1