2
votes

I have implemented something violating the MVVM pattern, and I wondered if there was a MVVM way of doing this.

I have a Window MainWindow, its DataContext is bound to a class called ViewModel which implements INotifyPropertyChanged.

I also implemented a Window ChildWindow which appears in a "Dialog" style when a button is clicked, using a RelayCommand. The DataContext of ChildWindow also binds to ViewModel. This Window is used to fill the details of a new list Item. I pass the View as a CommandParameter to the ViewModel, so that the ChildWindow can be centered in comparison to the MainWindow. This is not MVVM, and I would like to change this.

First, I implemented this in a non-MVVM way: Here is my XAML for the button in MainWindow which opens the ChildWindow:

 <Button Name="BtnInsert" Width="50" Margin="10" Command="{Binding OpenChildWindowCommand}" CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}">Add</Button>

Here is my simplified XAML for the ChildWindow:

<Window x:Class="HWE_Einteilen_Prototype.View.ListItemWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:HWE_Einteilen_Prototype.View"
        mc:Ignorable="d"
        Title="test" Height="400" Width="400">
    <TextBox Width="50" Text="{Binding CurrentListItem.Id}" ></TextBox>
</Window>

And here is my (simplified) ViewModel Class:

public class ViewModel : INotifyPropertyChanged
{
    private DataContext _ctx;
    private ListItem _currentListItem;
    private ObservableCollection<listItem> _listItems;

    private ListItemWindow _listItemWindow;
    private ICommand _openListItemWindowCommand;

    public event PropertyChangedEventHandler PropertyChanged;

    public ObservableCollection<ListItem> ListItems
    {
        get { return _listItems; }
        set
        {
            _listItems = value;
            OnPropertyChanged();
        }
    }

    public ListItem CurrentListItem
    {
        get { return _currentListItem; }
        set
        {
            _currentListItem = value;
            OnPropertyChanged();
        }
    }

    public ICommand OpenListItemWindowCommand
    {
        get { return _openListItemWindowCommand; }
        set
        {
            _openListItemWindowCommand = value;
            OnPropertyChanged();
        }
    }

    public ViewModel()
    {
        OpenListItemWindowCommand = new RelayCommand(this.OpenNewListItemWindow, this.CanOpenListItemWindow);
    }

    private void OpenNewListItemWindow(object parameter) 
    {
        CurrentListItem = new listItem(){Id = "testId"};
        _listItemWindow = new StListItemWindow(){DataContext = this};     
        _listItemWindow.Owner = (MainWindow)parameter;                   
        _listItemWindow.WindowStartupLocation = WindowStartupLocation.CenterOwner;
        _listItemWindow.Closing += OnStListItemWindowClosing;    
        _listItemWindow.Show();
    }

    private bool CanOpenListItemWindow(object parameter) 
    {
        return true;
    }


    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

What I have tried: I have tried implementing a Behavior (from system.windows.interactivity) for the button opening the child window, so that it creates a new Window and does all the centering and owner stuff, and leaving only CurrentListItem = new listItem(){Id = "testId"}; in the command method. However, in this case binding to CurrentListItem in the ChildWindow throws an exception.

XAML Code for the MainWindow Button:

    <Button Name="BtnInsert" Width="50" Margin="10" Command="{Binding OpenListItemWindowCommand}" Content="Add">
        <i:Interaction.Behaviors>
            <behaviors:BehButtonNewWindow></behaviors:BehButtonNewWindow>
        </i:Interaction.Behaviors>
    </Button>

Behavior Code:

class BehButtonNewWindow : Behavior<Button>
{
    private StListItemWindow _ListItemWindow;
    protected override void OnAttached()
    {
        AssociatedObject.Click += OnClickHandler;
    }

    protected override void OnDetaching()
    {
        AssociatedObject.Click -= OnClickHandler;
    }
    private void OnClickHandler(object sender, RoutedEventArgs routedEventArgs)
    {
        if (sender is Button button)
        {
            var win = Window.GetWindow(button);

            if (win != null)
            {
                _ListItemWindow = new ListItemWindow
                {
                    DataContext = win.DataContext,
                    Owner = win,
                    WindowStartupLocation = WindowStartupLocation.CenterOwner
                };

                _ListItemWindow.Show();
            }

        }
    }
}

Code of Command Execute Method from ViewModel:

    private void OpenNewStListItemWindow(object parameter) 
    {
        CurrentListItem = new ListItem(){Id = "testId"}; 
}

What am I doing wrong?

1
Possible duplicate of Opening new window in MVVM WPFSinatr
Concerning mvvm violation - you can take a look to the stackoverflow.com/questions/42931775/is-mvvm-pattern-brokenRekshino
@Sinatr it's not a duplicate. The question you linked to is concerned with creating a new Window for a new ViewModel instance. The question I have is creating a new Window binding to the SAME ViewModel instance.A. Non
But it is still creating new window with some sort of ViewModel attached to said window.XAMlMAX
Opening a window is a UI concern. Simply handle the button click in the codebehind, construct a new window and stick the current VM in it. MVVM != no codebehind.user1228

1 Answers

1
votes

Credit for this answer goes to Will (see comments)

On handling the window opening:

Opening a window is a UI concern. Simply handle the button click in the codebehind, construct a new window and stick the current VM in it. MVVM != no codebehind.

On handling vm code:

[...] If you mean that last little bit of code at the bottom, make it public and have the window call it before opening the new window. The UI is perfectly fine knowing about your view models. They're designed to display their state and bind to their properties.

Thanks for your help!