EDCD / EDDI

Companion application for Elite Dangerous
Other
448 stars 82 forks source link

Split EDDI project into EddiCore and EddiApp #1781

Open richardbuckle opened 4 years ago

richardbuckle commented 4 years ago

I propose to split the "EDDI" project into:

  1. "EddiCore", containing the stuff upon which the monitors and services depend, and
  2. "EddiApp", being purely the UI that runs up standalone.

Rationale:

  1. Banishes the "F5 doesn't update everything problem". This re-jig allows "EddiApp" to depend upon all code-generating projects, so that having "EddiApp" as the startup project means that good old F5 once again works to build everything necessary before debugging.
  2. It allows for adding shared resources to "EddiCore" that all UI-displaying projects can access.
  3. It allows for XAML resource dictionaries in "EDDICore" that will let me push down shared stuff like the alternating table AliceBlue into the proper place.

Code impact:

richardbuckle commented 4 years ago

EDDI Core App split

GioVAX commented 4 years ago

I started splitting the DLLs. Initial findings to collect the team thoughts on possible approaches:

Probably not the right moment, but this whole work would be a no brainer if we had a Dependency Injection framework in place.

Tkael commented 4 years ago

Fair points.

GioVAX commented 4 years ago

About the monitor vs responder for updating the UI, are you implying that other parts of the application do read the state from the (e.g.) squadron tab of the app UI?

My assumption was that some monitor (journal?) would get the original event, hand it to the EDDI object, and then all the responders would get notified and do whatever they need to do. For the new app UI responder, that would be doing the update.

I also assumed that any other part of the app would get any necessary state from the EDDI object, not from the app UI.

Tkael commented 4 years ago

No. Global state is as you see it in EDDI.cs. It's not tangled up in the front end UI thank goodness.

I'm referring to the OnEvent() method in EDDI.cs:

        private async void OnEvent(Event @event)
        {
            // We send the event to all monitors to ensure that their info is up-to-date
            // All changes to state must be handled here, so this must be synchronous
            passToMonitorPreHandlers(@event);

            // Now we pass the data to the responders to process asynchronously, waiting for all to complete
            // Responders must not change global states.
            await passToRespondersAsync(@event);

            // We also pass the event to all active monitors in case they have asynchronous follow-on work, waiting for all to complete
            await passToMonitorPostHandlersAsync(@event);
        }

In the case of squadrons, there is a global state variable in EDDI.cs called "SquadronStarSystem". I think it would be appropriate to update that variable with a PreHandler from a Monitor, before the value is passed to responders like the Speech Responder or the VoiceAttack Responder.

GioVAX commented 4 years ago

Apologies I wasn't clear :) I was not suggesting nor stating that the state is managed in the UI.

I was referring to code like the following (line 2241-> 2249 from EDDI.cs)

// Update the squadron UI data
Application.Current?.Dispatcher?.Invoke(() =>
{
       if (Application.Current?.MainWindow != null)
      {
             ((MainWindow)Application.Current.MainWindow).eddiSquadronNameText.Text = theEvent.name;
             ((MainWindow)Application.Current.MainWindow).squadronRankDropDown.SelectedItem = rank.localizedName;
      }
});

The code above is sandwiched between other instructions that do update the EDDI state, while this appears to me to be just updating the UI and could be handled by having a responder in the MainWindow code.

I am aware I have very little knowledge of the codebase, so I'd appreciate if you could point out what I am missing 😃

Tkael commented 4 years ago

Agreed, the highlighted code ought to be moved.

The section immediately below the section you highlighted

            // Update the commander object, if it exists
            if (Cmdr != null)
            {
                Cmdr.squadronname = theEvent.name;
                Cmdr.squadronrank = rank;
            }

updates a global state.

Since updating global states needs to happen before responders act on those global states, I think that it would be appropriate to consider a "Commander Monitor", with the UI for the commander tab being updated from that monitor, rather than a "Commander Responder".

Tkael commented 4 years ago

It occurs to me that there may be a simpler approach. We could set up two way bindings between the UI elements you identified and the Cmdr object, making provisions to update the configuration file when the properties of the Cmdr object have changed. Thoughts?

GioVAX commented 4 years ago

WRT the Monitor/Responder dichotomy, my understanding was that monitors wait on external events (e.g. new journal entries) and create internal events, while responders act on internal events and perform application actions. But based on your comments I am starting to think that I got this wrong 😄 Can you shed some more light on this, please?

About the 2-way bindings, is it implemented by the following functions (from CargoMonitor)?

public void EnableConfigBinding(MainWindow configWindow)
{
    configWindow.Dispatcher.Invoke(() => { BindingOperations.EnableCollectionSynchronization(inventory, inventoryLock); });
}

public void DisableConfigBinding(MainWindow configWindow)
{
     configWindow.Dispatcher.Invoke(() => { BindingOperations.DisableCollectionSynchronization(inventory); });
}
Tkael commented 4 years ago

Yes, BindingOperations.EnableCollectionSynchronization is part of how we enable two way binding for the observable collection inventory in the Cargo Monitor. https://docs.microsoft.com/en-us/dotnet/framework/wpf/data/how-to-create-and-bind-to-an-observablecollection Please also note that some bound items in EddiCargoMonitor/ConfigurationWindow.xaml have the mode set to "TwoWay" to allow editing either in the front end or in the back end. https://docs.microsoft.com/en-us/dotnet/framework/wpf/data/how-to-specify-the-direction-of-the-binding

As for Monitors and Responders... your understanding is generally correct... new data typically comes in through either a monitor rather than through a responder. Many monitors, however, also play a critical role in managing state data (e.g. the Cargo monitor, the Shipyard monitor, the Material monitor).

Both monitors and responders act on events and the timing of those actions is important. Monitors act first to pre-handle events and set states. Then responders then act to handle events referencing those global states. Finally, monitors post-handle the events as required.

Example:

GioVAX commented 4 years ago

So, if I get this correctly, Monitors and Responders are quite a different concept from Producers and Consumers of events.

They actually represent stages of event processing: Monitor:PreHandle --> Responder:Handle --> Monitor:PostHandle. In addition to this, Responders:Handle should not update the state so that they can be executed in parallel.

Tkael commented 4 years ago

Correct. A monitor can be a producer but isn't necessarily so.

richardbuckle commented 4 years ago

Erroneously closed.

barrywimlett commented 3 years ago

Certainly my first impression with this codebase is that two large libraries 'Utilities' and 'DataDefinitions' have a whole host of sub dependencies/requirements such as RollBar UI and Mathlib. Breaking these large libraries into smaller chunks would then mean that consuming code projects are not encumbered by the restrictions of those 3rd part externals. ie. anything that wants DataDefintions needs MathLib when only one class 'StarClass' uses that library.

My experience is that libraries with vauge names involving words like core/common/util often contain a hodge-podge of functionaility often unrelated to each other.

Also separating the XAML code from functional code is a good idea.

richardbuckle commented 3 years ago

'Utilities' and 'DataDefinitions' are stable, well-defined and have low churn. We are happy with the structure there and that they represent a sensible separation of concerns for our use case.

Everything is going to depend on RollBar because it is our crash and logging telemetry.

Everything is going to depend on MathLib because, well, we need to do math.

Thank you for the input but we are not going to restructure a well-working project based on purely doctrinal principles.