Yes, I've done this in the past with the ActualWidth
and ActualHeight
properties, both of which are read-only. I created an attached behavior that has ObservedWidth
and ObservedHeight
attached properties. It also has an Observe
property that is used to do the initial hook-up. Usage looks like this:
<UserControl ...
SizeObserver.Observe="True"
SizeObserver.ObservedWidth="{Binding Width, Mode=OneWayToSource}"
SizeObserver.ObservedHeight="{Binding Height, Mode=OneWayToSource}"
So the view model has Width
and Height
properties that are always in sync with the ObservedWidth
and ObservedHeight
attached properties. The Observe
property simply attaches to the SizeChanged
event of the FrameworkElement
. In the handle, it updates its ObservedWidth
and ObservedHeight
properties. Ergo, the Width
and Height
of the view model is always in sync with the ActualWidth
and ActualHeight
of the UserControl
.
Perhaps not the perfect solution (I agree - read-only DPs should support OneWayToSource
bindings), but it works and it upholds the MVVM pattern. Obviously, the ObservedWidth
and ObservedHeight
DPs are not read-only.
UPDATE: here's code that implements the functionality described above:
public static class SizeObserver
{
public static readonly DependencyProperty ObserveProperty = DependencyProperty.RegisterAttached(
"Observe",
typeof(bool),
typeof(SizeObserver),
new FrameworkPropertyMetadata(OnObserveChanged));
public static readonly DependencyProperty ObservedWidthProperty = DependencyProperty.RegisterAttached(
"ObservedWidth",
typeof(double),
typeof(SizeObserver));
public static readonly DependencyProperty ObservedHeightProperty = DependencyProperty.RegisterAttached(
"ObservedHeight",
typeof(double),
typeof(SizeObserver));
public static bool GetObserve(FrameworkElement frameworkElement)
{
frameworkElement.AssertNotNull("frameworkElement");
return (bool)frameworkElement.GetValue(ObserveProperty);
}
public static void SetObserve(FrameworkElement frameworkElement, bool observe)
{
frameworkElement.AssertNotNull("frameworkElement");
frameworkElement.SetValue(ObserveProperty, observe);
}
public static double GetObservedWidth(FrameworkElement frameworkElement)
{
frameworkElement.AssertNotNull("frameworkElement");
return (double)frameworkElement.GetValue(ObservedWidthProperty);
}
public static void SetObservedWidth(FrameworkElement frameworkElement, double observedWidth)
{
frameworkElement.AssertNotNull("frameworkElement");
frameworkElement.SetValue(ObservedWidthProperty, observedWidth);
}
public static double GetObservedHeight(FrameworkElement frameworkElement)
{
frameworkElement.AssertNotNull("frameworkElement");
return (double)frameworkElement.GetValue(ObservedHeightProperty);
}
public static void SetObservedHeight(FrameworkElement frameworkElement, double observedHeight)
{
frameworkElement.AssertNotNull("frameworkElement");
frameworkElement.SetValue(ObservedHeightProperty, observedHeight);
}
private static void OnObserveChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
var frameworkElement = (FrameworkElement)dependencyObject;
if ((bool)e.NewValue)
{
frameworkElement.SizeChanged += OnFrameworkElementSizeChanged;
UpdateObservedSizesForFrameworkElement(frameworkElement);
}
else
{
frameworkElement.SizeChanged -= OnFrameworkElementSizeChanged;
}
}
private static void OnFrameworkElementSizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateObservedSizesForFrameworkElement((FrameworkElement)sender);
}
private static void UpdateObservedSizesForFrameworkElement(FrameworkElement frameworkElement)
{
// WPF 4.0 onwards
frameworkElement.SetCurrentValue(ObservedWidthProperty, frameworkElement.ActualWidth);
frameworkElement.SetCurrentValue(ObservedHeightProperty, frameworkElement.ActualHeight);
// WPF 3.5 and prior
////SetObservedWidth(frameworkElement, frameworkElement.ActualWidth);
////SetObservedHeight(frameworkElement, frameworkElement.ActualHeight);
}
}
The thing is that if you were following MVVM, you would have a BookViewModel
for your Book
model class. So you would have a INotifyPropertyChanged
implementation on that view model. Exactly for that purpose MVVM exists (but not only).
That being said, the INotifyPropertyChanged
has to be implemented on view model classes, not models.
UPDATE: In response to your update and our discussion in comments...
By BookViewModel
I meant something else. You need to wrap in this view model not the whole collection of Book
objects but an individual Book
:
public class BookViewModel : INotifyPropertyChanged
{
private Book book;
public Book Book {
get { return book; }
}
public string Title {
get { return Book.Title; }
set {
Book.Title = value;
NotifyPropertyChanged("Title");
}
}
public BookViewModel(Book book) {
this.book = book;
}
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(String info) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
And your BookProvider
will return ObservableCollection<BookViewModel>
instead of ObservableCollection<Book>
:
public class BookProvider
{
public ObservableCollection<BookViewModel> GetBooks() {
ObservableCollection<BookViewModel> books = new ObservableCollection<BookViewModel>();
books.Add(new BookViewModel(new Book {
Title = "Book1",
Authors = new List<Author> { new Author { Name = "Joe" }, new Author { Name = "Phil" } }
}));
books.Add(new BookViewModel(new Book {
Title = "Book2",
Authors = new List<Author> { new Author { Name = "Jane" }, new Author { Name = "Bob" } }
}));
return books;
}
}
As you can see, when you are updating the Title
property of the Book
you will be doing it through the Title
property of the corresponding view model that will raise the PropertyChanged
event, which will trigger the UI update.
Best Solution
Kent wrote an interesting blog about this topic: View Models: POCOs versus DependencyObjects.
Short summary:
I prefer the POCO approach. A base class for PresentationModel (aka ViewModel) which implements INotifyPropertyChanged interface can be found here: http://compositeextensions.codeplex.com