Patrick Desjardins
Patrick Desjardins

Reputation: 140743

Silverlight with MVVM : How to access Event of the ViewModel from the View?

I have a MVVM application and somewhere in the application our company use a Third-Party that cannot use {Binding}. It's a component that draw shapes, etc. What I want it, when the ViewModel load from the persisted storage all shapes to notify the View to draw them. In a perfect world I would just have the take the Third-party and bind it to the ViewModel Shapes collection but I cannot.

From there, my idea was that I could get from the View the ViewModel (via the DataContext) and to hook the PropertyChanged event. The problem is that the DataContext is not yet initialized in the constructor, so it's NULL, and I cannot hook the event. Here is a sample of the code:

        public CanvasView()
        {
            InitializeComponent();
            ((CanvasViewModel)this.DataContext).PropertyChanged += new PropertyChangedEventHandler(CanvasView_PropertyChanged); //Exception Throw here because DataContext is null
        }

        void CanvasView_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "Shapes")
            {
                DrawShapes(); 
            }
        }

How can I get information from my ViewModel to my View in that case?

Upvotes: 2

Views: 1699

Answers (4)

jrwren
jrwren

Reputation: 17898

Use the DataContextChanged event on the View (Window or UserControl)

    public CanvasView()
    {
        InitializeComponent();
        Action wireDataContext += new Action ( () => {
            if (DataContext!=null) 
                      ((CanvasViewModel)this.DataContext).PropertyChanged += new PropertyChangedEventHandler(CanvasView_PropertyChanged);
            });
        this.DataContextChanged += (_,__) => wireDataContext();
        wireDataContext();
    }

    void CanvasView_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "Shapes")
        {
            DrawShapes(); 
        }
    }

update: Here is a documented way to get DataContextChanged in Silverlight 3 and 4 http://www.lhotka.net/weblog/AddingDataContextChangedInSilverlight.aspx

Upvotes: 0

Vladimir Dorokhov
Vladimir Dorokhov

Reputation: 3839

I want to comment Dennis Roche answer. Really, in this case we can use wrap approach, because we need to redraw view when Shapes collection changed. But view model logic can be too complex, and ,for instance, instead of redraw on PropertyChanged we should redraw on some custom event (f.i. ModelReloadEvent). In this case, wrapping doesn't help, but subscription on this event does, as in Muad'Dib solution - view model use event based communication with view, but this event should be view specific.

Using code-behind with View specific logic doesn't break MVVM. Yes, this code can be decorated with behavior/action, but using code behind - just simple solution.

Also, take a look at this view on MVVM. According to structure, ViewModel knows about abstract IView.If you worked with Caliburn/Caliburn.Micro MVVM frameworks you remember ViewAware class and IViewAware, which allows get view in view model.

So, more flexible solution I guess is next:

View:

public class CanvasView() : ICanvasView
{
        public CanvasView()
        {
            InitializeComponent();
        }

        public void DrawShapes()
        {
          // implementation
        }
}

ICanvasView:

public interface ICanvasView
{
    void DrawShapes();
}

CanvasViewModel:

public class CanvasViewModel : ViewAware
{    
    private ObservableCollection<IShape> _shapes;
    public ObservableCollection<IShape> Shapes
    {
        get
        {
           return _shapes;
        }
        set
        {
           _shapes = value;
           NotifyOfPropertyChange(() => Shapes);
           RedrawView();
        }
    }

    private void RedrawView()
    {
        ICanvasView abstractView = (ICanvasView)GetView();
        abstractView.DrawShapes();
    }
}

Upvotes: 0

Dennis
Dennis

Reputation: 20561

All of the answers so far breaks the MVVM pattern with having code-behind on the view. Personally I would wrap the 3rd party control in a UserControl and then wire up a few dependency properties with property change events.

C#

public partial class MyWrappedControl : UserControl
{
  public static readonly DependencyProperty ShapesProperty = DependencyProperty.Register("Shapes", typeof(ObservableCollection<IShape>), typeof(MyWrappedControl),
      new PropertyMetadata(null, MyWrappedControl.OnShapesPropertyChanged);

  public ObservableCollection<IShape> Shapes
  {
    get { return (ObservableCollection<IShape>)GetValue(ShapesProperty); }
    set { SetValue(ShapesProperty, value); }
  }

  private static void OnShapesPropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
  {
    ((MyWrappedControl)o).OnShapesPropertyChanged(e);
  }

  private void OnShapesPropertyChanged(DependencyPropertyChangedEventArgs e)
  {
    // Do stuff, e.g. shapeDrawer.DrawShapes(); 
  }
}

XAML

<UserControl 
    Name="MyWrappedControl"
    x:Class="MyWrappedControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
    <!-- your control -->
    <shapeDrawerControl x:Name="shapeDrawer" />
</UserControl>

Upvotes: 4

Muad&#39;Dib
Muad&#39;Dib

Reputation: 29196

you could also attach your handler in the Loaded event.

public CanvasView()
{
   InitializeComponent();
   this.Loaded += this.ViewLoaded;
}

void ViewLoaded(object sender, PropertyChangedEventArgs e)
{
   ((CanvasViewModel)this.DataContext).PropertyChanged += new PropertyChangedEventHandler(CanvasView_PropertyChanged);    
}

void CanvasView_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
   if (e.PropertyName == "Shapes")
   {
      DrawShapes(); 
   }
}

Upvotes: 2

Related Questions