-1

I have this view (fragment) with 2 buttons. If I click the left button, View 2 opens. If I click the right button, View 3 opens. I am using Caliburn Micro. Hence, the Button's x:Name value is the name of the View Model's method which is invoked after clicking the button.

View1:

enter image description here

     <StackPanel Name="PnlButtons" 
                Grid.Row="1"  
                Grid.ColumnSpan="2"
                HorizontalAlignment="Center"
                Orientation="Horizontal"
                Opacity="1">
        <Button x:Name="ArtikelAuswahl" 
                Background="Bisque" 
                Content="Artikel auswählen" 
                Width="170" Height="25"
                FontFamily="Verdana">
        </Button>
        <Button x:Name="SonderAuswahl" 
                Background="BlanchedAlmond" 
                Content="Sonderartikel hinzufügen" 
                Width="170" Height="25"
                FontFamily="Verdana">
        </Button>
    </StackPanel>

Here are the 2 methods that get invoked after button click. You can see they have the same name. Now people say that it is prohibited to open a view inside a view model. This is why I am using an IWindowManager instance winmanager inside my methods when I want to open a new view. Instead of creating a new view instance, I create a new viewmodel instance! First question: Is this against the rules of MVVM?

ViewModel1:

    public class CreateLieferscheinViewModel : Conductor<object>
    {

        private IWindowManager winmanager = new WindowManager();
        public InventurartikelViewModel inventur = new InventurartikelViewModel(); 
        public SonderartikelViewModel sonder = new SonderartikelViewModel();

        public void ArtikelAuswahl()
        {          
            wwinmanager.ShowWindow(inventur, null, null);        
        }

        public void SonderAuswahl()
        {
            winmanager.ShowWindow(sonder,null,null);
        }

        /* ToBeImplemented: Invoke this method once `Artikelliste` is filled!!! */
        public void ArtikellisteUmformen()
        {
            for (int k = 0; k < inventur.Artikelliste.Count; k++)
        {
            Artikelsammlung.Add(new ArtikelModel()); //every selected article will get added to Artikelsammlung
            //get each selected article unfiltered (unformatted)
            Artikelsammlung[k].Bezeichnung = inventur.Artikelliste[k].ToString();
            //Extract the unit out of the Artikel-String
            Artikelsammlung[k].Einheit = Zeichenketten.TextFindenVonBisEnde(Artikelsammlung[k].Bezeichnung, "<", ">");

           //remove "in <Einheit>" from the Artikel-String
            Artikelsammlung[k].Bezeichnung = Zeichenketten.EinheitEntfernen(Artikelsammlung[k].Bezeichnung);
            /* 
             * Bezeichnung and Einheit are now properly formatted...
             */
        }
    }

}

    private ObservableCollection<ArtikelModel> _artikelsammlung;
    public ObservableCollection<ArtikelModel> Artikelsammlung
    {
        get { return _artikelsammlung; }
        set
        {
            _artikelsammlung = value;
            OnPropertyChanged("Artikelsammlung");
        }
    }

Ok, now let's say the ArtikelAuswahl gets invoked. Thanks to Caliburn Micro, View2 shows:

View2:

enter image description here

<Window x:Class="Lieferscheine.Views.InventurartikelView"
         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:local="clr-namespace:Lieferscheine.Views"
         xmlns:main="clr-namespace:Lieferscheine"
         xmlns:cal="http://www.caliburnproject.org"
         mc:Ignorable="d" Title="Inventurartikel suchen"
         Height="450" Width="370">
<StackPanel Height="423" VerticalAlignment="Bottom">
    <Label Name="lblArtikelbezeichnung" Content="Artikelbezeichnung:" Margin="20, 20, 20, 0"></Label>
    <TextBox Name="BezText" 
             Width="Auto" 
             Margin="20, 0, 20, 0"
             IsEnabled="{Binding Path=BezEnabled}"
             cal:Message.Attach="[Event KeyUp] = [Action KeyUpBez($executionContext)]">
    </TextBox>

    <Label Name="lblLieferant" Content="Lieferant:" Margin="20, 0, 20, 0"></Label>
    <TextBox Name="LiefText" 
             Width="Auto" 
             Margin="20, 0, 20, 0"
             IsEnabled="{Binding Path=LiefEnabled}"                
             cal:Message.Attach="[Event KeyUp] = [Action KeyUpLief($executionContext)]">
    </TextBox>

    <Button Name="SucheArtikel" 
            Content="Suchen" 
            Width="100" Height="25" 
            Margin="20, 10,240, 10">
    </Button>
    <Button x:Name="GesamteListeAnzeigen" 
            Content="Gesamte Liste anzeigen" 
            Width="150" Height="26" 
            Margin="0, -50, 20, 0" 
            HorizontalAlignment="Right"/>
    <main:MultipleSelectionListBox 
             x:Name="LboxAddArtikel"                
             SelectionMode="Multiple" 
             Width="320" Height="220" 
             Margin="20, 10, 20, 10"
             BindableSelectedItems="{Binding Path=MyCollectionOfSelectedIDs}">
    </main:MultipleSelectionListBox>

    <Button x:Name="FuegeArtikelHinzu" 
            Content="Hinzufügen" 
            Width="100" Height="25">
    </Button>
</StackPanel>

View2 is data bound to ViewModel2. But before I show you ViewModel2 I want to show you what I can do in View2:

enter image description here

I select 3 articles from a listbox and click on the button on the bottom of view2 to add these articles to a list:

<Button x:Name="FuegeArtikelHinzu" Content="Hinzufügen" Width="100" Height="25"> </Button>

The articles are added to a list in the method FuegeArtikelHinzu in viewmodel2:

ViewModel2:

public class InventurartikelViewModel : Screen
{
    private List<string> _artikelliste = new List<string>();
    public List<string> Artikelliste
    {
        get { return _artikelliste; }
        set
        {
            _artikelliste = value;
            OnPropertyChanged("Artikelliste");
        }
    }

    public bool ArtikellisteUpdated()
    {
        Filled = Artikelliste != null ? true : false;

        return Filled;
    }

    private bool _filled;
    public bool Filled
    {
        get 
        { 
            return _filled; 
        }
        set
        {
            _filled = value;
            OnPropertyChanged("Filled");
            if(_filled == true)
            {
                //TO BE IMPLEMENTED
                //then invoke ViewModelA's method `ArtikellisteUmformen()`
            }
            _filled = false; //I believe I would need to set _filled back 

            //to false to prevent overflow. Otherwise ViewModelA's method 

            //`ArtikellisteUmformen()` would get invoked over and over again because 

            //_filled is always true from now on. Is that correct?
        }

    }
    public void FuegeArtikelHinzu()
    {
        try
        {
            //This adds only the multiple selected items to a list
            var multi = MyCollectionOfSelectedIDs;

            int i = 0;
            foreach (string item in multi)
            {
                Artikelliste.Insert(i, item);
                i++;
            }

            MessageBox.Show("Artikel hinzugefügt!"); //ok, all added... 

            //call `ArtikellisteUpdated()` and set Property `Filled` (=`Artikelliste` is filled) to true
            ArtikellisteUpdated(); //true unless null

        }
        catch (Exception e)
        {
            MessageBox.Show(e.Message, "Zuerst Artikel auswählen!"); //you must select an article first...
        }
    }
}

And here is my problem! I need the selected articles in the list artikelliste IN MY VIEW MODEL1!!! But I ran into a dead end. I heard that this is solved by implementing IMessenger service but I don't understand how it works in my example. What do I need to do to pass the artikelliste to ViewModel1 according to my example? If you are not familiar with Caliburn Micro, post another solution, either from scratch or with a framework, I don't mind. Thanks in advance for any help!

EDIT:

I can now access ViewModel2 and its Artikelliste property within ViewModel1. However, I would like to invoke ViewModel1's method ArtikellisteUmformen() as soon as ViewModel2's Artikelliste got updated. How do I do that? This is what I would like to do:

When the Artikellistehas been filled, you can invoke an event on ViewModel2, for example ArtikelListeUpdated. ViewModel1 listens to that event and reacts to it if necessary. You even don't need the event in case you don't have to react to it immediately.

timunix
  • 609
  • 6
  • 19
  • First question, as long as there are no UI elements exposed by the windowManager it doesn't violates the MVVM. If you want the selected items then add a command per item in the list box to add them into collection of your ViewModel. Because you have the instance of the view model you will have access to the items. simple – XAMlMAX Jan 22 '20 at 14:50
  • I updated my question. I did what @maxx313 suggested except for the last step (bullet point). I don't know how to implement the event and the listener. Never done that before. Could you please help? Please implement the last bullet point into my project given. It is very important to me! – timunix Jan 23 '20 at 06:24
  • Question Update: Now the last step I need is: When ViewModel2's Property `Filled` is true, then invoke ViewModel1's method `ArtikellisteUmformen()`. But I cannot access VIewModel1 from ViewModel2. HOW CAN I LISTEN FROM VIEWMODEL1 TO VIEWMODEL2's PROPERTY `FILLED` AND INVOKE `ARTIKELLISTEUMFORMEN()` WHEN `FILLED == TRUE`???????????????????? – timunix Jan 23 '20 at 09:36

1 Answers1

1
  1. Creating a ViewModel instance and using the WindowManager interface is not against MVVM. You should be fine with that.

  2. I am not familiar with the Caliburn Micro Framework. But what about this:

    • Hold the artikelliste as a property in ViewModel2.
    • Hold an instance of ViewModel2 as a property in ViewModel1. You pass that instance when invoking the WindowManager.ShowWindow method.
    • Then, when the artikelliste has been filled, you can invoke an event on ViewModel2, for example ArtikelListeUpdated. ViewModel1 listens to that event and reacts to it if necessary. You even don't need the event in case you don't have to react to it immediately.

Edit: Example for the last step:

public class ViewModel1 {
    public ViewModel2 ChildVm {get;} = new ViewModel2();

    public ViewModel1() {
        ChildVm.Updated += OnChildUpdated;
    }

    private void OnChildUpdated(object pSender, EventArgs pArgs) {
        // do what is needed
    }
}

public class ViewModel2 {
    public event EventHandler Updated;

    public void DoStuff()
    {
        // do something

        if (Updated != null)
            Updated.Invoke(this, EventArgs.Empty);
    }
}

Be aware that ViewModel2 has an internal reference to ViewModel1 in this case, therefore preventing ViewModel1 from being garbage-collected.

I recommend you look at the basics of EventHandling for C#, before proceeding with your project: Understanding events and event handlers in C#

maxx313
  • 51
  • 4
  • "Then, when the artikelliste has been filled, you can invoke an event on ViewModel2, for example ArtikelListeUpdated. ViewModel1 listens to that event and reacts to it if necessary." Could you add an example for this, please? This is the only part I am missing. I need to invoke a method in ViewModel1 once the artikelliste is filled. – timunix Jan 22 '20 at 16:57
  • Holy F! It is working!!! After 5 days, thank you so much sir! – timunix Jan 23 '20 at 10:57