movra
Posts: 70
Joined: 02 Apr 2014, 20:35

Re: [Unity] Binding to an object other than BaseComponent

24 Jul 2014, 07:57

In the meantime I know I can write Views to bind ViewModel properties to XAML controls, but it's redundant and error-prone. Would be a much more elegant solution if you could just plug in the ViewModel.
I have no idea what you are talking about here, can you elaborate?
In uFrame Element designer we create a TestChat element, add the Input and Output properties and the SayHelloCommand. We also connect a TestChatView to the element. This will create TestChatViewModel.cs, TestChatController.cs and TestChatView.cs.

Image


Now we can create the following XAML (derived from the sample Command.xaml). This is what I meant by plugging in the ViewModel.
<Grid
    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" mc:Ignorable="d"
    xmlns:local="clr-namespace:TestChat">
     
    <Grid.Resources>
        <local:TestChatViewModel x:Key="TestChatViewModel"/>
    </Grid.Resources>
 
     <Viewbox>
       <StackPanel DataContext="{StaticResource TestChatViewModel}">
          <TextBox Text="{Binding Input, Mode=TwoWay}"/>
          <Button Content="Say Hello" Command="{Binding SayHelloCommand}" />
          <TextBlock Text="{Binding Output}" />
       </StackPanel>
     </Viewbox>
</Grid>
If Noesis GUI did not have the BaseComponent requirement and if uFrame was compliant with the WPF API, then we were done now. The controls in the XAML would have been bound to the Input and Output properties and the SayHelloCommand command, which were automatically generated as seen in my previous post.

Since that is currently not the case, we have to bind the controls to the ViewModel properties ourselves. We also need to rip-out the bindings from the XAML and add Names to the controls instead so we can find them. The TestChatView.cs would look like this:
using UnityEngine;
using Noesis;
 
public partial class TestChatView
{
    private NoesisGUIPanel _noesisGUIPanel;
    private Grid _grid;
    private TextBox _inputTextbox;
    private Button _SayHelloButton;
    private TextBlock _OutputTextblock;
 
    public override void Awake()
    {
        base.Awake();
 
        _noesisGUIPanel = GetComponent<NoesisGUIPanel>();
        _grid = _noesisGUIPanel.GetRoot<Grid>();

        _inputTextbox = _grid.FindName<TextBox>("textbox_Input");
        _SayHelloButton = _grid.FindName<Button>("button_SayHelloCommand");
        _OutputTextblock = _grid.FindName<TextBlock>("textblock_Output");
         
        _SayHelloButton.Click += _SayHelloButton_Click;
    }   
 
    private void _SayHelloButton_Click(BaseComponent sender, RoutedEventArgs e)
    {
        ExecuteSayHelloCommand();
    }
 
    public override void InputChanged(string value)
    {
        base.InputChanged(value);
        _inputTextbox.SetText(value);
    }
 
    public override void OutputChanged(string value)
    {
        base.OutputChanged(value);
        if (string.IsNullOrEmpty(value))
        {
            _OutputTextblock.SetText(string.Empty);
        }
        else
        {
            _OutputTextblock.SetText(value);
        }
    }
 
    public void Update()
    {
        TestChat.Input = _inputTextbox.GetText();
    }
}
and the modified XAML
<Grid
    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" mc:Ignorable="d">
 
     <Viewbox>
       <StackPanel>
          <TextBox Name="textbox_Input"/>
          <Button Content="Say Hello" Name="button_SayHelloCommand" />
          <TextBlock Name="textblock_Output" />
       </StackPanel>
     </Viewbox>
</Grid>
I said the binding in TestChatView.cs is redundant because the bindings are already defined in the XAML and ViewModel. It's error-prone because that View needs to be manually synced with the XAML and ViewModel.

Finally you have to add TestChatView.cs to the Unity scene which would otherwise not be necessary.


Image

Image


So I have talked about this with uFrame's developer who told me the main problem is that Mono does not contain System.Windows.Input.ICommand nor System.Collections.Specialized.ObservableCollection. Remember that Noesis GUI also doesn't use System.Windows.Input.ICommand, but Noesis.BaseCommand instead. I wonder if there's a solution that solves this kind of solution for both Noesis and uFrame.

Here's a similar case about Implementing MVVM in WPF without using System.Windows.Input.ICommand.
 
User avatar
jsantos
Site Admin
Posts: 2903
Joined: 20 Jan 2012, 17:18
Contact:

Re: [Unity] Binding to an object other than BaseComponent

28 Jul 2014, 19:29

We are in contact with the uFrame developer about this. I expect to have a talk with him in Skype very soon. There are several things that must be solved in our side to improve the quality of the code generated although in the current state I think that this can be solved this way.

We think that the XAML must not be modified. The solution is using a wrapper around the ViewModel provided by uFrame. Although this wrapper can be written manually right now we think that it could be automatically generated by uFrame. The idea is the following:
[Noesis.Extended]
// This could be generated automatically by uFrame
public class TestChatViewModelNoesis : Noesis.BaseComponent
{
    private TestChatViewModel _viewModel;

    public TestChatViewModelNoesis(TestChatViewModel viewModel)
    {
        _viewModel = viewModel;
    }

    // ... wrap of ViewModel properties
}
And instead of using the VM provided by uFrame, use this one instead as the DataContext of the Xaml.
public partial class TestChatView
{
    public override void Awake()
    {
        base.Awake();
 
        var gui = GetComponent<NoesisGUIPanel>();
        var root = _noesisGUIPanel.GetRoot<Grid>();
        root.SetDataContext(new TestChatViewModelNoesis(this.uFrameTestChatViewModel);
    }

   // ...
}
A wrap for our BaseCommand is also needed. This wrap would be used by the class TestChatViewModelNoesis posted above. The idea would be:
[Noesis.Extended]
public class UFrameCommand: public Noesis.BaseCommand
{
    private UFrame::ICommand _command;

    public UFrameCommand(object sender, UFrame::ICommand command)
    {
        _command = command;
        _command.Sender = sender;
    }

    public bool CanExecuteCommand(Noesis.BaseComponent parameter)
    {
        return true;
    }

    public void ExecuteCommand(Noesis.BaseComponent parameter)
    {
        _command.Parameter = parameter; 
        _command.Execute();
    } 

   // ...
}
What do you think about this approach?
 
movra
Posts: 70
Joined: 02 Apr 2014, 20:35

Re: [Unity] Binding to an object other than BaseComponent

29 Jul 2014, 00:16

Thanks for looking into this subject thoroughly and discussing it with the uFrame developers!

You know, I started writing a similar wrapper, copying the properties by hand. But I got stuck associating the uFrame ViewModel to wrapper. You solved it by setting the DataContext to a new instance of the wrapper initialized with the existing ViewModel.

Also I see that if you set the DataContext in the XAML, it will look for a parameter-less constructor. It's quite ingenious then to set it programmatically in the uFrame View, especially since you still occasionally need a View to do things. For example disabling a button when textboxes in a form are empty, which in XAML would require MultiDataTrigger.

A nice side-effect is that you can now edit the XAML properly in Kaxaml. Sure you could use Blend after adding all the assemblies, but it's incredibly slow and bloated.

However I hit a few problems:
  • Collections: uFrame uses System.Collections.Generic.ICollection<T> and Noesis has its own Collection class which seems to be some sort of replacement for System.Collections.ObjectModel.ObservableCollection<T>. The different types of collection have to be converted somewhere.

    I wonder... Right now uFrame and Noesis both have their own different variations of Microsoft's MVVM classes that are not available in Mono. Wouldn't it a more elegant solution to port those classes to Mono? Instead of requiring specific code generators, wrappers and converters, Noesis and uFrame would be able to interact with each other and any other MVVM software out-of-the-box. But I have no idea how it would work in practice without breaking existing code.

    Maybe there is something useful in here: MVVMCross or here Mosa. What about this, faking ObservableCollection?
  • The data flow in the direction from Noesis to uFrame works. The uFrame View properties are updated with the state of the Noesis GUI. Unfortunately the other way around doesn't seem to work for me at the moment.

    In the View (CustomerView.cs):
        public override void FirstNameChanged(string value)
        {
            UpdateCanSave();
        }
    
        public override void LastNameChanged(string value)
        {
            UpdateCanSave();
        }   
    
        private void UpdateCanSave()
        {
            bool isValidForm = !(Customer.LastName.IsNullOrWhiteSpace() || Customer.FirstName.IsNullOrWhiteSpace());
            Customer.CanSave = isValidForm;
            Debug.Log("Can Save: " + isValidForm);
        }
    
        public override void CanSaveChanged(bool value)
        {
            Debug.Log("CanSaveChanged to: " + value);  
      }
    In the wrapper (CustomerViewModelNoesis.cs):
            public bool CanSave
            {
                get { return _customerViewModel.CanSave; }
                set
                {
                    if (_customerViewModel.CanSave != value)
                    {
                        _customerViewModel.CanSave = value;
                        NotifyPropertyChanged("CanSave");
                    }
                }
            }
    In the XAML (CustomerEditor.xaml)
    <Button Content="Save" IsEnabled="{Binding CanSave}" Command="{Binding SaveCommand}" />
    The debug.log messages will be shown in the console but the button won't change state.
 
movra
Posts: 70
Joined: 02 Apr 2014, 20:35

Re: [Unity] Binding to an object other than BaseComponent

29 Jul 2014, 00:46

Re: 2nd problem: I'm an idiot. When writing to the ViewModel, you should write to the wrapper, not directly to the ViewModel.
 
private CustomerViewModelNoesis _wrapper;

public override void Awake()
{
        base.Awake();

        _panel = GetComponent<NoesisGUIPanel>();
        _grid = _panel.GetRoot<Grid>();

        _wrapper = new CustomerViewModelNoesis(Customer);
        _grid.SetDataContext(_wrapper);
}

private void UpdateCanSave()
    {
        bool isValidForm = !(Customer.LastName.IsNullOrWhiteSpace() || Customer.FirstName.IsNullOrWhiteSpace());
        _wrapper.CanSave = isValidForm;
        Debug.Log("Can Save: " + isValidForm);
    }
This makes me think though. What if the ViewModel is written to from a different source, eg. a remote service? Seems like the wrapper needs bindings to the ViewModel too.
 
User avatar
jsantos
Site Admin
Posts: 2903
Joined: 20 Jan 2012, 17:18
Contact:

Re: [Unity] Binding to an object other than BaseComponent

29 Jul 2014, 17:21

  • Collections: uFrame uses System.Collections.Generic.ICollection<T> and Noesis has its own Collection class which seems to be some sort of replacement for System.Collections.ObjectModel.ObservableCollection<T>. The different types of collection have to be converted somewhere.
Yes, this is a problem right now, we need to solve it. For now, the fast hack is copying the elements from uFrame container to our ObservableCollection.

The proper solution is that noesisGUI understands the IList<T> interface, as uFrame is doing. That way, both containers would be compatible.
I wonder... Right now uFrame and Noesis both have their own different variations of Microsoft's MVVM classes that are not available in Mono. Wouldn't it a more elegant solution to port those classes to Mono? Instead of requiring specific code generators, wrappers and converters, Noesis and uFrame would be able to interact with each other and any other MVVM software out-of-the-box. But I have no idea how it would work in practice without breaking existing code.

Maybe there is something useful in here: MVVMCross or here Mosa. What about this, faking ObservableCollection?
Our idea is porting those classes to Mono and putting them under the Noesis namespace. uFrame developer will probably will want them under another namespace. The same for another thirdparty developers. I think that at the end bridge classes are needed. What you are suggesting is the ideal, if we found compromise with the uFrame developer the wrapper class won't be needed. Anyway I think that the automatic generation of the wrapper from the uFrame framework is a good compromise.
This makes me think though. What if the ViewModel is written to from a different source, eg. a remote service? Seems like the wrapper needs bindings to the ViewModel too.
Yes, a mechanism is needed to communicate changes from the inner properties to the wrapper. One more thing to the list. :)
 
movra
Posts: 70
Joined: 02 Apr 2014, 20:35

Re: [Unity] Binding to an object other than BaseComponent

06 Aug 2014, 18:06

Some randomly assorted thoughts.

In .NET 4 Microsoft moved the important MVVM classes to System.dll, except for ICommand which is not moved to System.dll until .Net 4.5. Mono already contains ports of those classes: https://github.com/mono/mono/tree/maste ... ass/System

Unity5 will feature IL2CPP, enabling Unity to upgrade to a newer version of Mono C#. Until then maybe you can include the Mono ports and subclass them?

There are some gotchas with WPF. For example, you mentioned copying elements to Noesis.Collection. You have to do that one item at a time. Every time you add one item Noesis does a P/Invoke and notifies the listeners.In some cases it would be better to be able to add a batch of items at once. Imagine updating a ListBox with 1000 items. The solutions to this problem and similar performance problems are found here: http://pelebyte.net/blog/2011/07/11/twe ... rformance/
 
User avatar
jsantos
Site Admin
Posts: 2903
Joined: 20 Jan 2012, 17:18
Contact:

Re: [Unity] Binding to an object other than BaseComponent

07 Aug 2014, 18:13

Some randomly assorted thoughts.
In .NET 4 Microsoft moved the important MVVM classes to System.dll, except for ICommand which is not moved to System.dll until .Net 4.5. Mono already contains ports of those classes: https://github.com/mono/mono/tree/maste ... ass/System

Unity5 will feature IL2CPP, enabling Unity to upgrade to a newer version of Mono C#. Until then maybe you can include the Mono ports and subclass them?
Yes, when talking with the uFrame developer we agreed that the best would creating a github repository with these interfaces. Both noesisGUI and uFrame will include that project in a folder named MVVM of the Unity Project.
There are some gotchas with WPF. For example, you mentioned copying elements to Noesis.Collection. You have to do that one item at a time. Every time you add one item Noesis does a P/Invoke and notifies the listeners.In some cases it would be better to be able to add a batch of items at once. Imagine updating a ListBox with 1000 items. The solutions to this problem and similar performance problems are found here: http://pelebyte.net/blog/2011/07/11/twe ... rformance/
The idea of copying elements mentioned in this thread should be considered a hack to have something working and experimental to play with but it is not the final solution. What we want to achieve is supporting the IList interface. As uFrame is going to do the same, collections will be able to be shared between both packages.

Thanks a lot for all your investigation. It is helping us a lot!

Who is online

Users browsing this forum: No registered users and 0 guests