Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a better way to update a DrawingVisual in a VisualCollection host in WPF with the MVVM pattern?

Tags:

c#

mvvm

wpf

I am working on a project in WPF that updates the view up to 30 times a second. I'm employing the MVVM pattern to the best of my knowledge and am fairly happy with the results so far. However I'm wondering if there is not a more efficient way to update my DrawingVisuals inside a VisualCollection in my host container. On every property change in my viewmodel, I am finding, removing then re-adding a new DrawingVisual for that viewmodel. With constantly moving objects I feel there should be a better way, such as binding the DrawingVisuals themselves directly to the viewmodel's properties, but what would that look like? As the number of models in the simulation grows, I need to ensure I have a streamlined workflow for updates. I started following the example here: http://msdn.microsoft.com/en-us/library/ms742254.aspx

I deliberately avoid DependencyProperties and binding UserControls to every viewmodel as I need a very efficient drawing canvas (hence the QuickCanvas below). So I have very little need for XAML other than to design the main UI and wiring up buttons and commands. Please ask if something seems unclear or I left out something important. Thanks!

The visual host container (the view):

public partial class QuickCanvas : FrameworkElement
{
    private readonly VisualCollection _visuals;
    private readonly Dictionary<Guid, DrawingVisual> _visualDictionary;

    public static readonly DependencyProperty ItemsSourceProperty =
        DependencyProperty.Register("ItemsSource", typeof(ObservableNotifiableCollection<IVisualModel>),
        typeof(QuickCanvas),
        new PropertyMetadata(OnItemsSourceChanged));

    public QuickCanvas()
    {
        InitializeComponent();
        _visuals = new VisualCollection(this);
        _visualDictionary = new Dictionary<Guid, DrawingVisual>();
    }

    public ObservableNotifiableCollection<IVisualModel> ItemsSource
    {
        set { SetValue(ItemsSourceProperty, value); }
        get { return (ObservableNotifiableCollection<IVisualModel>)GetValue(ItemsSourceProperty); }
    }

    protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
        base.OnPropertyChanged(e);
        if (e.Property.Name == "Width" || e.Property.Name == "Height" || e.Property.Name == "Center")
        {
            UpdateVisualChildren();
        }
    }

    private void UpdateVisualChildren()
    {
        if (ItemsSource == null || _visuals.Count == 0) return;

        foreach (var model in ItemsSource)
        {
            var visual = FindVisualForModel(model);
            if (visual != null)
            {
                UpdateVisualFromModel(visual, model);
            }
        }
    }

    private void UpdateVisualPairFromModel(DrawingVisual visual, IVisualModel model)
    {
        visual.Transform = ApplyVisualTransform(visual.Transform, model);
    }

    private static void OnItemsSourceChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
        (obj as QuickCanvas).OnItemsSourceChanged(args);
    }

    private void OnItemsSourceChanged(DependencyPropertyChangedEventArgs args)
    {
        _visuals.Clear();

        if (args.OldValue != null)
        {
            var models = args.OldValue as ObservableNotifiableCollection<IVisualModel>;
            models.CollectionCleared -= OnCollectionCleared;
            models.CollectionChanged -= OnCollectionChanged;
            models.ItemPropertyChanged -= OnItemPropertyChanged;
        }

        if (args.NewValue != null)
        {
            var models = args.NewValue as ObservableNotifiableCollection<IVisualModel>;
            models.CollectionCleared += OnCollectionCleared;
            models.CollectionChanged += OnCollectionChanged;
            models.ItemPropertyChanged += OnItemPropertyChanged;

            CreateVisualChildren(models);
        }
    }

    private void OnCollectionCleared(object sender, EventArgs args)
    {
        _visuals.Clear();
        _visualDictionary.Clear();
    }

    private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
    {
        if (args.OldItems != null)
            RemoveVisualChildren(args.OldItems);

        if (args.NewItems != null)
            CreateVisualChildren(args.NewItems);
    }

    private void OnItemPropertyChanged(object sender, ItemPropertyChangedEventArgs args)
    {
        var model = args.Item as IVisualModel;
        if (model == null)
            throw new ArgumentException("args.Item was expected to be of type IVisualModel but was not.");
        //TODO is there a better way to update without having to add/remove visuals?
        var visual = FindVisualForModel(model);
        _visuals.Remove(visual);
        visual = CreateVisualFromModel(model);
        _visuals.Add(visual);
        _visualDictionary[model.Id] = visual;**
    }

    private DrawingVisual FindVisualForModel(IVisualModel model)
    {
        return _visualDictionary[model.Id];
    }

    private void CreateVisualChildren(IEnumerable models)
    {
        foreach (IVisualModel model in models)
        {
            var visual = CreateVisualFromModel(model);
            _visuals.Add(visual);
            _visuals.Add(visual);
            _visualDictionary.Add(model.Id, visual);
        }
    }

    private DrawingVisual CreateVisualFromModel(IVisualModel model)
    {
        var visual = model.GetVisual();
        UpdateVisualFromModel(visual, model);
        return visual;
    }

    private void RemoveVisualChildren(IEnumerable models)
    {
        foreach (IVisualModel model in models)
        {
            var visual = FindVisualForModel(model);
            if (visual != null)
            {
                _visuals.Remove(visual);
                _visualDictionary.Remove(model.Id);
            }
        }
    }

    protected override int VisualChildrenCount
    {
        get
        {
            return _visuals.Count;
        }
    }

    protected override Visual GetVisualChild(int index)
    {
        if (index < 0 || index >= _visuals.Count)
            throw new ArgumentOutOfRangeException("index");

        return _visuals[index];
    }
}

An IVisuaModel impl:

public class VehicleViewModel : IVisualModel
{
    private readonly Vehicle _vehicle;
    private readonly IVisualFactory<VehicleViewmodel> _visualFactory;
    private readonly IMessageBus _messageBus;

    public VehicleViewmodel(Vehicle vehicle, IVisualFactory<VehicleViewmodel> visualFactory, IMessageBus messageBus)
    {
        _vehicle = vehicle;
        _visualFactory = visualFactory;
        _messageBus = messageBus;
        _messageBus.Subscribe<VehicleMovedMessage>(VehicleMoveHandler, Dispatcher.CurrentDispatcher);
        Id = Guid.NewGuid();
    }

    public void Dispose()
    {
        _messageBus.Unsubscribe<VehicleMovedMessage>(VehicleMoveHandler);
    }

    private void VehicleMoveHandler(VehicleMovedMessage message)
    {
        if (message.Vehicle.Equals(_vehicle))
            OnPropertyChanged("");
    }

    public Guid Id { get; private set; }
    public Point Anchor { get { return _vehicle.Position; } }
    public double Rotation { get { return _vehicle.Orientation; } }

    public DrawingVisual GetVisual()
    {
        return _visualFactory.Create(this);
    }

    public double Width { get { return _vehicle.VehicleType.Width; } }
    public double Length { get { return _vehicle.VehicleType.Length; } }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        var handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

A IVisualFactory impl:

public class VehicleVisualFactory : IVisualFactory<VehicleViewModel>
{
    private readonly IDictionary<string, Pen> _pens;
    private readonly IDictionary<string, Brush> _brushes;

    public VehicleVisualFactory(IDictionary<string, Pen> pens, IDictionary<string, Brush> brushes)
    {
        _pens = pens;
        _brushes = brushes;
    }

    public DrawingVisual Create(VehicleViewmodel viewModel)
    {
        var result = new DrawingVisual();
        using (var context = result.RenderOpen())
        {
            context.DrawRectangle(_brushes["VehicleGreen"], _pens["VehicleDarkGreen"],
                new Rect(-viewModel.Width / 2, -viewModel.Length / 2, viewModel.Width, viewModel.Length));
        }
        return result;
    }
}
like image 808
bpforsich Avatar asked Feb 10 '13 20:02

bpforsich


1 Answers

I found you approach very interesting and cleverly made while reading your post. I have already made some few experimentations with wpf and "real time" problematic, here are the few things I could bring from my own experience:

  • I will not recommand you to use full bound views with you viewmodel, especialy the properties of your vehicules if the amount of vehicule varies. Indeed, the binding mecanism is very very fast... only once it has been initialized. It is indeed quite slow to initialize. So if you tend to use such mecanism, may I suggest you using pooling to avoid as much as possible binding allocation. Reagarding your question on the appearance it would have... I guess all the binding related things would be performed in you vehicul factory in c# (http://msdn.microsoft.com/en-us/library/ms742863.aspx)?

  • My second point is half a clue half a question: have you considered using another architecture pattern than MVVM? I can see in your code that you have implemented lot of things related to view/viewModel synchronization. MVVM is a structure that enables interfacing and separating easily highly interactive views with data layer thanks to ViewModel principle. Games architecture is often not tending to separates the same concerns than forms applications mostly because performance, scalability and modularity concerns are not the same challenges. That's why I was wondering whether agent pattern, classical getInputs/think/draw vehicule objects would not be a good approach if your aim is to have most performance -> meaning one layer application and several layers in agents.

Hope this helped a little. Do not hesitate if there are some points you don't understand or you simply desagree. Take us informed, I am very interested in the choices you'll make!

like image 95
Charles HETIER Avatar answered Oct 26 '22 23:10

Charles HETIER