-
Notifications
You must be signed in to change notification settings - Fork 76
Eto.Forms
Pulsar no longer uses Eto.Forms for ui, however I'm loath to delete this page compleatly as it still contains some good generic Eto.Forms info, which could be useful for others googling Eto.Forms stuff.
See also https://github.com/Pulsar4xDevs/Pulsar4x/wiki/IMGUI.Net for info on the library we're now using for UI
To start creating Eto forms you'll want to get the Eto.Forms Visual Studio Addin. This will give you the templates for creating a new view. It's pretty easy, just follow the simple instructions here: https://github.com/picoe/Eto/wiki/Quick-Start
Once that's downloaded, installed and VS has restarted, you should be able to Add, New Item, Eto.Forms... you've got three options from there: Eto.Forms Panel Eto.Forms Panel(Json) Eto.Forms Panel(Xaml)
I'm writing this wiki as I do this myself, so I'm guessing here, but I think the plain Panel is like you're oldschool winforms where you do it all in the 'codebehind' Panel(Json) and Panel(Xaml) are your more wpf/mvvm style, described in json or xaml respectively. I'm going to have a go at converting my previously created wpf ScientistUC to eto so I'm going to try Panel(xaml) and hope I don't have to do too much to convert it.
Next you'll get a set of options for the base class, I'm choosing Panel here since it's a usercontrol that's meant to be a subcomponent of a form.
after VS has requested installing the xaml serialiser or whatever it was and blah blah, you should get a split screen view, the top is a visual feedback of the xaml code you write in teh bottom half.
first gotcha is that <Grid> in Eto is an abstract class, you'll want one of it's children. I went with <TableLayout>
second gotcha is that you add objects to the cells instead of the WPF way of setting the row and column on the object ie:
WPF:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="24"/>
<RowDefinition Height="24"/>
</Grid.RowDefinitions>
</Grid>
<Label Grid.Row="0" Grid.Column="0" Text="FirstLabel" />
<Label Grid.Row="0" Grid.Column="1" Text="SecondLabel" />
ETO:
<TableLayout>
<TableRow>
<Label Text="FirstLabel" />
<Label Text="SecondLabel" />
</TableRow>
</TableLayout>
Eto doesn't yet have full databinding in the xaml, there is a limited amount however:
<Label Text ="{Binding ParameterName}" />
<NumericUpDown Value="{Binding ParameterName}"/>
<CheckBox Checked ="{Binding ParameterName}" />
Remember to set the datacontext to the viewmodel in the xeto.cs.
more technical databinding needs to be done in the xeto.cs, and typically uses fancy lambda.
note that StackLayout doesn't have databinding, and ListBox doesn't support custom controls. (hopefully we'll get a ListView in the future which will work like ListBox but support custom controls?)
Eto supports dual way databinding as well as one way (either direction). There are two forms of databinding: direct and indirect (delegated).
Delegated dualbinding is what you will use to bind the form elements to the VM, via the dataContext variable (every form control has this). Most form elements have simple shortcuts to bind properties indirectly. Indirect is also what you must use if you write the bindings in xeto.
Direct is useful if you want to bind two completely arbitrary properties together (same type of course), or if you need to bind on some property or subproperty that is not directly accessible on the VM. For example, if you set the VM as the dataContext (this is the normal way to do things), you cannot indirectly bind to inner data of that VM. Eg, If a VM has the property StarSystem, you can bind that to a form element (say a select box of star systems) and it is handled fine, but you cannot indirectly bind to StarSystem's properties, say StarSystems.Stars. That requires a direct binding.
check out Views/SystemView for one example
check here for more info: https://github.com/picoe/Eto/wiki/Data-Binding
Idealy, you want to be able to bind the viewmodel from the xaml:
<c:myCustomFooView x:Name="MyCusomFoo" DataContext:"{Binding MyFooVMProperty}"/>
this works well when you have a parent view with child views. the parent viewmodel has a public property:
public FooVM MyFooVMProperty {get;set;}
sometimes you have to handle the viewmodel as a specific type in the xeto.cs though. The easiest way is to have the xeto.cs constructor take the viewmodel as a parameter, or to to have a public method which takes the viewmodel and sets the datacontext:
public void SetViewmodel(FooVM viewmodel)
{ DataContext = viewmodel; }
doing this may be necessary if you've got a StackLayout that is going to hold a number of custom views.
It is possible to cast the datacontext to the VM type, and check for the DataContextChanged: this has become my preferred way of handling needing to access the VM from the 'behind code'
public class CargoStorageView : Scrollable
{
StackLayout CargoTypes;
public CargoStorageView()
{
XamlReader.Load(this);
DataContextChanged += CargoStorageView_DataContextChanged;
}
private void CargoStorageView_DataContextChanged(object sender, EventArgs e)
{
if (DataContext is CargoStorageVM)
{
CargoTypes.Items.Clear();
CargoStorageVM dc = (CargoStorageVM)DataContext;
foreach (var item in dc.CargoStore)
{
CargoTypeStoreView typesView = new CargoTypeStoreView();
typesView.DataContext = item;
CargoTypes.Items.Add(typesView);
}
}
}
}
In WPF Xaml we set the control name thus:
<Label Name="MyFooName"/>
In xeto we typically do it with ID:
<Label ID="MyFooName"/>
or with x:Name
<c:JobAbilityView x:Name="RefinaryAbilityView" />
Note the above is a custom control, at the moment there's a bug with the portable.xaml which requires us to use the above method instead of the ID for custom controls. also note that to use custom controls in the xeto you'll need to set the namespace for it, ie as for the above example setting c as the namespace :
<Panel
xmlns="http://schema.picoe.ca/eto.forms"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:Pulsar4X.CrossPlatformUI.Views;assembly=Pulsar4X.CrossPlatformUI"
>
Dictionaries are a pain, even in WPF. I've created a DictionaryVM in the ViewModels namespace.
this inherits from IDictionary but has an ObservibleCollection DisplayList to databind your view to.
when initialising the DictionaryVM set whether it should display the keys or the values:
DictionaryVM myDict = new DictionaryVM<SomeRandomType, string>(DisplayMode.Key); (or DictionaryVM<string, SomeRandomType>(DisplayMode.Value))
to get the key or the value:
var someVal = myDict.GetValue(indexOfDisplayListEntry); (or mydict.GetKey respectively)
It uses two dictionaries to keep track of the kvp to index relationship, so while it's fairly quick to lookup it's probably not particularly memory efficient.
bind it to a combobox in the constructor of the xeto.cs like this:
myComboBox.BindDataContext(c => c.DataStore, (DictionaryVM<Guid, string> m) => m.DisplayList);
myComboBox.SelectedIndexBinding.BindDataContext((DictionaryVM<Guid, string> m) => m.SelectedIndex);
note that the above does not need to know what the viewmodel/datacontext type is.
set the datacontext preferably in the in the xaml:
<ComboBox ID="myComboBox" DataContext="{Binding myDictionaryVM}" />
Or in the behindcode exto.cs :
myComboBox.DataContext = viewModel.myDictionaryVM;
you can then do fancy stuff in the viewmodel like listening for the myDictionaryVM.SelectionChangedEvent which will give you what the index was, and what it is now. you can use the SelectedKey property to get the object at the currently selected index, or the SelectedValue for the current value.
The EntityManager.ManagerSubpulses has a threadsafe* event handler you can subscribe to which will fire when the managersubpulse changes the date for entities under its control (ie the date within the system).
This should happen whenever any data gets changed via normal processing.
*This is threadsafe, i.e. it will marshal the event to the UI thread if there is a UI thread. (Normally the event would be fired on the thread that invoked it)
You can access an entites managers managersubpulse this way:
myEntity.Manager.ManagerSubpulses.SystemDateChangedEvent += OnDateChange;
More complex handling of triggering an update still has to be sorted out: https://github.com/Pulsar4xDevs/Pulsar4x/issues/177
yeah this is an annoying one, I'm going to start adding things to this list as I come across them:
- ComboBox is binding datastore instead of datacontext, if this is the case it'll normaly throw where the datacontext is set. ie
<ComboBox ID="myComboBox" DataStore="{Binding myDictionaryVM}" />is incorrect if you're binding to a POCO and setting the binding in the constructor. it should be:<ComboBox ID="myComboBox" DataContext="{Binding myDictionaryVM}" />if you're binding it directly to a list or ObservibleCollection, then use DataStore.
Check your viewmodel, bindings need to be made to properties with a public getter not a field. ie:
public DictionaryVM<object, string> myDictionaryVM {get;} = new DictionaryVM<object,string>(Displaymode.Value); //this is good
public DictionaryVM<object, string> myDictionaryVM; //this won't work.
The number of times I've made this mistake, and wasted soooo much time trying to figure out what was wrong...
Is it an datagrid cell? those don't update on propertychanged yet. you'll have to tell the datagrid to reload the row (or rows).
double check your viewmodel is implementing INoifyPropertyChanged (or inherits from something that does)
double check that you're making a call to OnPropertyChanged(); in the property setter, if you're not doing it in the property setter, then you can do OnPropertyChanged(nameof(PropertyName));
Check your xeto.cs constructor makes the call to XamlReader.Load(this); before binding controls
check your namespace. the namespace must be the same as your directory hierarchy. also, VS doesn't handle renaming xeto classes well, you may have to manually edit the .csproj file to get everything looking right. double check spelling!
Documentation
-
Contribution
-
Design
-
Processes
-
Datatypes
-
Guides and Resources
-
Modding & Json files