0

I am developing a VS package extension for functionality that used to be in a VS addin.

The addin would load files into a Toolbar window, and then if the user double clicked on an item (which was the name of a file) the file would be opened in the editor in VS.If the user right-clicked on an item, it would give a pop-up menu. So my question is about what the best way would be to connect these actions (the double-click and right-click) of the listbox items with my existing code.

For the extension we use WPF, but for the addin it was windows forms. However, I am not very familiar with WPF. About a year ago, I watched Brian Noyes's Pluralsight course, "WPF MVVM In Depth" and implemented some things in the extension, but then I haven't worked on the extension for most of this year. The result is that I only have vague recollections of the code I wrote, and I am a bit confused as to what the best design would be.

So let me show you what I already have:

Here is the XAML file:

<UserControl x:Class="Sym.VisualStudioExtension.Engines.TAEngineView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:behaviours="clr-namespace:Sym.VisualStudioExtension"
         xmlns:local="clr-namespace:Sym.VisualStudioExtension"
         local:ViewModelLocator.AutoWireViewModel="True"
         mc:Ignorable="d" 
         d:DesignHeight="700" d:DesignWidth="400">
<Grid>
    <TabControl x:Name="tabControl" HorizontalAlignment="Left" Height="490" Margin="19,44,-36,-234" VerticalAlignment="Top" Width="317">
        <TabItem Header="Parameter Files">
            <ListBox Margin="20" ItemsSource="{Binding ParameterFilesList}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="{Binding Path=Name}" />
                        </StackPanel>
                    </DataTemplate>
                </ListBox.ItemTemplate>                    
            </ListBox>
        </TabItem>
        <TabItem Header="Calc Files">
            <ListBox Margin="20" ItemsSource="{Binding CalcFilesList}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="{Binding Path=Name}" />
                        </StackPanel>
                    </DataTemplate>
                </ListBox.ItemTemplate>                   
            </ListBox>
        </TabItem>
    </TabControl>
    <Label x:Name="label" Content="{Binding Path=Title}" HorizontalAlignment="Left" Margin="19,13,0,0" VerticalAlignment="Top" Width="367
           " BorderThickness="2"/>

</Grid>

CalcFilesList is of type ObservableCollection<CalcFile>, and ParameterFilesList of type ObservableCollection<Parameter>.

Then I already have this RelayCommand class:

using System;
using System.Diagnostics;
using System.Windows.Input;

namespace Sym.VisualStudioExtension
{
/// <summary>
/// A command whose sole purpose is to 
/// relay its functionality to other
/// objects by invoking delegates. The
/// default return value for the CanExecute
/// method is 'true'.
/// </summary>
public class RelayCommand : ICommand
{
    #region Fields

    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;        

    #endregion // Fields

    #region Constructors

    /// <summary>
    /// Creates a new command that can always execute.
    /// </summary>
    /// <param name="execute">The execution logic.</param>
    public RelayCommand(Action<object> execute)
        : this(execute, null)
    {
    }

    /// <summary>
    /// Creates a new command.
    /// </summary>
    /// <param name="execute">The execution logic.</param>
    /// <param name="canExecute">The execution status logic.</param>
    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");

        _execute = execute;
        _canExecute = canExecute;           
    }

    #endregion // Constructors

    #region ICommand Members

    [DebuggerStepThrough]
    public bool CanExecute(object parameters)
    {
        return _canExecute == null ? true : _canExecute(parameters);
    }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public void Execute(object parameters)
    {
        _execute(parameters);
    }

    #endregion // ICommand Members
}

public class RelayCommand<T> : ICommand
    {
        #region Fields

        private readonly Action<T> _execute = null;
        private readonly Predicate<T> _canExecute = null;

        #endregion

        #region Constructors

        /// <summary>
        /// Creates a new command that can always execute.
        /// </summary>
        /// <param name="execute">The execution logic.</param>
        public RelayCommand(Action<T> execute)
            : this(execute, null)
        {
        }

        /// <summary>
        /// Creates a new command with conditional execution.
        /// </summary>
        /// <param name="execute">The execution logic.</param>
        /// <param name="canExecute">The execution status logic.</param>
        public RelayCommand(Action<T> execute, Predicate<T> canExecute)
        {
            if (execute == null)
                throw new ArgumentNullException("execute");

            _execute = execute;
            _canExecute = canExecute;
        }

        #endregion

        #region ICommand Members

        public bool CanExecute(object parameter)
        {
            return _canExecute == null ? true : _canExecute((T)parameter);
        }

        public event EventHandler CanExecuteChanged
        {
            add
            {
                if (_canExecute != null)
                    CommandManager.RequerySuggested += value;
            }
            remove
            {
                if (_canExecute != null)
                    CommandManager.RequerySuggested -= value;
            }
        }

        public void Execute(object parameter)
        {
            _execute((T)parameter);
        }

        #endregion
    }
}

And this BindableBase class:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;

namespace Sym.VisualStudioExtension
{
    public class BindableBase : INotifyPropertyChanged
    {
        protected virtual void SetProperty<T>(ref T member, T val, [CallerMemberName] string propertyName = null)
        {
            if (object.Equals(member, val)) return;

            member = val;
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        public event PropertyChangedEventHandler PropertyChanged = delegate { };
        protected virtual void OnPropertyChanged(string propertyName)
        {         
                PropertyChanged(this, new     PropertyChangedEventArgs(propertyName));     
        }

    }
}

And here is the ViewModelLocator:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.Practices.Unity;
using Symplexity.VisualStudioExtension.Engines;

namespace Sym.VisualStudioExtension
{
    public static class ViewModelLocator
    {
        public static bool GetAutoWireViewModel(DependencyObject obj)
        {
            return (bool)obj.GetValue(AutoWireViewModelProperty);
        }

        public static void SetAutoWireViewModel(DependencyObject obj, bool value)
        {
            obj.SetValue(AutoWireViewModelProperty, value);
        }

        // Using a DependencyProperty as the backing store for AutoWireViewModel.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty AutoWireViewModelProperty =
            DependencyProperty.RegisterAttached("AutoWireViewModel", typeof(bool), typeof(ViewModelLocator), new PropertyMetadata(false, AutoWireViewModelChanged));

        private static void AutoWireViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (DesignerProperties.GetIsInDesignMode(d)) return;
            var viewType = d.GetType();
            var viewTypeName = viewType.FullName;
            var viewModelTypeName = viewTypeName + "Model";
            var viewModelType = Type.GetType(viewModelTypeName);


            if (viewModelTypeName.Contains("UtilitiesViewModel"))
            {
                 UtilitiesViewModel uViewModel = ContainerHelper.Container.Resolve<UtilitiesViewModel>();
                ((FrameworkElement)d).DataContext = uViewModel;
            }
            else
            {
                var viewModel = ContainerHelper.Container.Resolve(viewModelType); 
                ((FrameworkElement)d).DataContext = viewModel;
            }           
        }
    }
}

I have seen quite a few other threads concerning Listbox items and mouse events etc. So much so that I got confused between which route to go.

Various options

ItemContainerStyles&Commands

I guess having something in the code behind is not so bad, and it looks fairly easy for someone like me who has forgotten the little bit I knew about WPF and MVVM, but since I already have the RelayCommand, BindableBase and ViewModelLocator, it feels as if it would be better designed to connect the mouse events (double-click and right-click) with Commands, but I'm not quite sure how. So, assuming I have a method OpenFile in the TAEngineViewModel which should open the underlying file whose Name is shown in the item of the ListBox (if it is double-clicked) in the VS Editor, what should I put in the XAML? How do I pass the CalcFile/ParameterFile object that is selected, through to the TAEngineViewModel?

I assume the right-click event will be similar to the double-click, if not, how will it be different?

Community
  • 1
  • 1
Igavshne
  • 699
  • 7
  • 33

1 Answers1

0

As I can understand you need a mechanism that will take the list-item click/doubleclick events and will redirect it to the specific method inside your view model. Here is my suggestion:

  1. Find a modern and comfortable MVVM framework called Caliburn.Micro via nuget and install it into your project (you can use the nuget console to install the package, here is the explanation link https://www.nuget.org/packages/Caliburn.Micro),.
  2. In case you dont have a view model that is wired to your TAEngineView view you should create it. This view model will have to be called TAEngineViewModel, since your view model locator is based on the naming convention.
  3. Create methods called OnItemClick, and OnItemDoubleClick inside your view model.
  4. Use a specific caliburn syntax to redirect an click event to the specific method inside your view model here is the link with more commands https://caliburnmicro.codeplex.com/wikipage?title=Cheat%20Sheet&referringTitle=Documentation.

Here is a view-model code:

public class TAEngineViewModel:IOnClickSupport
{
    private readonly ObservableCollection<ItemWithName> _parameterList;
    public ObservableCollection<ItemWithName> CalcFilesList => _parameterList;
    public ObservableCollection<ItemWithName> ParameterFilesList => _parameterList;

    public TAEngineViewModel()
    {
        _parameterList = new ObservableCollection<ItemWithName>
        {
            new ItemWithName(this) {Name = "AAAAAA"},
            new ItemWithName(this) {Name = "BBBBBB"},
            new ItemWithName(this) {Name = "CCCCCC"},
            new ItemWithName(this) {Name = "DDDDDD"}
        };
    }

    public void OnClick(object args)
    {

    }

    public void OnDoubleClick(object args )
    {

    }
}

public interface IOnClickSupport
{
    void OnClick(object args);
    void OnDoubleClick(object args);
}

public class ItemWithName:BaseObservableObject
{
    private readonly IOnClickSupport _support;
    private string _name;

    public ItemWithName(IOnClickSupport support)
    {
        _support = support;
    }

    public string Name
    {
        get { return _name; }
        set
        {
            _name = value;
            OnPropertyChanged();
        }
    }

    public void OnClick(object args)
    {
        _support.OnClick(args);
    }

    public void OnDoubleClick(object args)
    {
        _support.OnDoubleClick(args);
    }
}

Here is the view code:

<UserControl x:Class="SoCaliburnInvolvedWithEventsToCommand.TAEngineView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:cal="http://www.caliburnproject.org"
         xmlns:soCaliburnInvolvedWithEventsToCommand="clr-namespace:SoCaliburnInvolvedWithEventsToCommand"
         soCaliburnInvolvedWithEventsToCommand:ViewModelLocator.AutoWireViewModel="True">
<UserControl.Resources>
    <DataTemplate x:Key="DataTemplateWithTextBlockInside">
        <TextBlock Text="{Binding Path=Name}" />
    </DataTemplate>
    <DataTemplate x:Key="ItemTemplate">
        <StackPanel Orientation="Horizontal">
            <ContentControl cal:Message.Attach="[Event MouseDoubleClick] = [Action OnDoubleClick($eventArgs)];[Event MouseRightButtonDown] = [Action OnClick($eventArgs)]"
                            Content="{Binding}"
                            ContentTemplate="{StaticResource DataTemplateWithTextBlockInside}" />
        </StackPanel>
    </DataTemplate>
</UserControl.Resources>
<Grid>
    <TabControl x:Name="tabControl"
                    Width="317"
                    Height="490"
                    Margin="19,44,-36,-234"
                    HorizontalAlignment="Left"
                    VerticalAlignment="Top">
        <TabItem Header="Parameter Files">
            <ListBox Margin="20"
                         ItemTemplate="{StaticResource ItemTemplate}"
                         ItemsSource="{Binding ParameterFilesList}" />
        </TabItem>
        <TabItem Header="Calc Files">
            <ListBox Margin="20"
                         ItemTemplate="{StaticResource ItemTemplate}"
                         ItemsSource="{Binding CalcFilesList}" />
        </TabItem>
    </TabControl>
    <Label x:Name="label"
               Width="367            "
               Margin="19,13,0,0"
               HorizontalAlignment="Left"
               VerticalAlignment="Top"
               BorderThickness="2"
               Content="{Binding Path=Title}" />

</Grid></UserControl>

The downside of the solution above is the fact that there is a cyclic relation under the hood(the view model is delivered to each sub-item in order to support the sub-item with main OnClick/OnDoubleClick logic), you can avoid this relation by using a kind of event-aggregation. I can suggest you to use the RX extension. It is a flexible way to connect between two objects. In order to start using the RX you should instal it via Nuget in your VS2015.

Here is an example:

public class TAEngineViewModel:IDisposable
{
    private IList<IDisposable> _disposablesChildrenList = new List<IDisposable>();
    private readonly ObservableCollection<ItemWithName> _parameterList;
    public ObservableCollection<ItemWithName> CalcFilesList => _parameterList;
    public ObservableCollection<ItemWithName> ParameterFilesList => _parameterList;

    public TAEngineViewModel()
    {
        _parameterList = new ObservableCollection<ItemWithName>
        {
            new ItemWithName {Name = "AAAAAA"},
            new ItemWithName {Name = "BBBBBB"},
            new ItemWithName {Name = "CCCCCC"},
            new ItemWithName {Name = "DDDDDD"}
        };
        Subscribe(_parameterList);

    }

    private  void Subscribe(ObservableCollection<ItemWithName> parameterList)
    {
        foreach (var itemWithName in parameterList)
        {
            var onRightClickObservableSubscription = itemWithName.OnRightClickObservable.Subscribe(OnClick);
            var onDoubleClickObservableSubscription = itemWithName.OnDoubleClickObservable.Subscribe(OnDoubleClick);
            _disposablesChildrenList.Add(onDoubleClickObservableSubscription);
            _disposablesChildrenList.Add(onRightClickObservableSubscription);
        }
    }

    public void OnClick(IItemArguments args)
    {
        Debug.WriteLine($"{args.SpecificItemWithName.Name} evet {args.SpecificEventArgs.GetType().Name}");
    }

    public void OnDoubleClick(IItemArguments args )
    {
        Debug.WriteLine($"{args.SpecificItemWithName.Name} evet {args.SpecificEventArgs.GetType().Name}");
    }

    public void Dispose()
    {
        foreach (var disposable in _disposablesChildrenList)
        {
            disposable.Dispose();
        }
    }
}

public interface IItemArguments
{
    ItemWithName SpecificItemWithName { get;}
    object SpecificEventArgs { get;}
}

public class ItemArguments : IItemArguments
{
    public ItemArguments(ItemWithName item, object args)
    {
        SpecificItemWithName = item;
        SpecificEventArgs = args;
    }

    public ItemWithName SpecificItemWithName { get; }

    public object SpecificEventArgs { get; }
}

public class ItemWithName:BaseObservableObject
{
    private string _name;

    private Subject<IItemArguments> _onDoubleClick = new Subject<IItemArguments>();
    private Subject<IItemArguments> _onClick = new Subject<IItemArguments>();
    public IObservable<IItemArguments> OnDoubleClickObservable;
    public IObservable<IItemArguments> OnRightClickObservable;

    public ItemWithName()
    {
        OnDoubleClickObservable = _onDoubleClick.AsObservable();
        OnRightClickObservable = _onClick.AsObservable();
    }

    public string Name
    {
        get { return _name; }
        set
        {
            _name = value;
            OnPropertyChanged();
        }
    }

    public void OnClick(object args)
    {
        _onClick.OnNext(new ItemArguments(this, args));
    }

    public void OnDoubleClick(object args)
    {
        _onDoubleClick.OnNext(new ItemArguments(this, args));
    }
}

As you can see, each time the method OnClick/OnDoubleClick is called, the specific(selected) item passed to the view-model. That is all. Let me know if you need more explanations. Best regards.

Community
  • 1
  • 1
Ilan
  • 2,762
  • 1
  • 13
  • 24
  • Thanks, for the time being I am simply using the code-behind, but I might return to this answer in the future. – Igavshne Dec 12 '16 at 12:41