3

EDIT:

Because I was to late with awarding the initial bounty of 300 to @arcain I'm reopening. And awarding the additional 150 to @arcain. Unless of course somebody provides even a better answer. :)

/ EDIT

Consider the following form:

language | region | active | default |
-----------------------------------------------
en       | GB     | [x]    | (*)     | [X delete]
nl       | NL     | [x]    | ( )     | [X delete]
nl       | BE     | [x]    | ( )     | [X delete]

[x] let visitors browser-settings determine the default language

[save]

The settings of the above table will be saved in a DB table which columns map to the above columns (excluding the last column obviously).

All (save & delete) actions direct to a Localization controller. The Localization controller basically calls methods on a LocalizationService, like so:

$localizationService->updateCollection( $_POST ) // update collection settings
// or
$localizationService->delete( $_POST ) // delete a single locale

The LocalizationService in it's turn calls a LocaleMapperDb, something like this:

foreach( $localeCollection as $locale )
{
    $localeMapperDb->update( LocaleModel $locale );
}
// or
$localeMapperDb->delete( LocaleModel $locale );

Where though, is the responsibility for saving this setting:

[x] let visitors browser-settings determine default language

It will be saved in a DB table called site_settings. I have thought of a few options:

  • Use a SiteService / SiteSettingsService in the LocalizationController. But then, the complete form is generated and processed in the LocalizationService already.
  • Use a SiteMapperDb / SiteSettingsMapperDb in the LocalizationService and use it in updateCollection( $_POST )
  • Use a SiteMapperDb / SiteSettingsMapperDb in the LocaleMapperDb

The first and last options look like the worst options, but I'm unsure. What do you feel is the best option? Or maybe you have an alternative option, I haven't thought of?

Decent Dabbler
  • 22,532
  • 8
  • 74
  • 106
  • I've put up a bounty for this question, since I got no response and very little views for this question to date. The former could be due to the possible unclarity of the question. If this is the case, please tell me, and I'll gladly adjust the question to see if can formulate the problem clearer. – Decent Dabbler Jan 22 '11 at 18:12
  • How come you never awarded the bounty? Can you comment on what was wrong with the answers? It seems like @arcain put a lot of work into this, and if it weren't for my upvote bringing him to 2 just as the bounty ended, he wouldn't have received anything. – jamesmortensen Jan 30 '11 at 01:54
  • @jmort253: I haven't made up my mind yet. I presume I can still award the bounty if I vote an answer as the best right? I've looked a the answers, but haven't found the piece of mind yet (bit busy with other stuff right now) to critically assess if the answer is useful enough for my situation. Don't worry though, if I choose one, I'll see to it that the answer gets awarded the points (whether automatically, or by me assigning extra points). – Decent Dabbler Jan 30 '11 at 02:10

1 Answers1

2

I think projecting domain model objects onto view model objects works well in this situation.

In the case of the attached code (please pardon me for writing it in C#; it should be fairly portable) the domain model objects are never exposed (they only are accessed directly within the service objects.) The services only expose view model objects, like LocalViewModel, and those view model objects are manipulated by the controllers.

The LocaleConfigController also maps the data returned by the services into a LocaleConfigViewModel object, and this object is the only object that is exchanged directly with the view.

So, in a nutshell, the view has a dedicated controller, and the view communicates with the controller via the LocaleConfigViewModel object. The controller manipulates the LocaleConfigViewModel object and calls into implementations of the ILocaleConfigService and the ISystemConfigService. The service objects never expose the domain model to the controller, and they are responsible for mapping view model objects to domain model objects (via whatever persistence mechanism is desirable.)

Note that the locale service is a configuration service, it would not have any implementation to look up what the correct localized strings would be. I would put that into another service, because it could be used in places where you would not want to expose any methods that would allow the alteration of the localization config.

For example, in the management side of the application, you would want both the localization config service and localization string rendering service (since the management site could be localized as well.) For a customer facing front end, you would instead probably want only the localization string rendering service, because system configuration modifications should be unwanted and out of scope for that site.

So, to finally answer your question: the controller contains references to both the locale and system configuration services, and the controller is dedicated to the view -- it has a well-defined contract where only LocaleConfigViewModels are exchanged.

As for where the responsibility lies for saving the system-wide settings, the controller is responsible for unpacking the system settings from the LocaleConfigViewModel and pushing them into the appropriate services (in this case the ISystemConfigService instance) where they will be persisted.

class LocaleViewModel
{
  public int Id;
  public string Language;
  public string Region;
  public bool Enabled;
  public bool Deleted;
}

class LocaleConfigViewModel
{
  public bool UseVisitorBrowserLocale;
  public LocaleViewModel DefaultLocale;
  public List<LocaleViewModel> Locales; 
}

class LocaleConfigController : ILocaleConfigController
{
  ILocaleConfigService localeConfig;
  ISystemConfigService systemConfig;

  public void Save(LocaleConfigViewModel model)
  {
    foreach (var locale in model.Locales)
    {
      if (locale.Deleted)
      {
        localeConfig.DeleteLocale(locale);
        continue;
      }
      localeConfig.UpdateLocale(locale);
    }
    systemConfig.DefaultLocaleId = model.DefaultLocale.Id;
    systemConfig.UseVisitorBrowserLocale = model.UseVisitorBrowserLocale;
  }

  public LocaleConfigViewModel GetCurrentView()
  {
    var model = new LocaleConfigViewModel();
    model.Locales = localeConfig.Locales;
    model.DefaultLocale = model.Locales.FirstOrDefault(l => l.Id == systemConfig.DefaultLocaleId);
    model.UseVisitorBrowserLocale = systemConfig.UseVisitorBrowserLocale;
    return model;
  }

  // ...
}

interface ILocaleConfigController
{
  void Save(LocaleConfigViewModel model);
  LocaleConfigViewModel GetCurrentView();
  // ... 
}

interface ILocaleConfigService // services will be stateless and threadsafe
{
  void DeleteLocale(LocaleViewModel locale);
  void UpdateLocale(LocaleViewModel locale);
  List<LocaleViewModel> Locales { get; }
  // ...
}

interface ISystemConfigService // services will be stateless and threadsafe
{
  int DefaultLocaleId { get; set; }
  bool UseVisitorBrowserLocale { get; set; }
  // ...
}
arcain
  • 14,920
  • 6
  • 55
  • 75
  • @arcain: Although your answer doesn't map one on one to my environment (PHP's Zend Framework) as easily as I had hoped, I still feel this is a valuable answer. ZF controllers can't easily receive ViewModels from a request. I should have given it more thought before tagging the question language-agnostic. Anyway, because of me postponing choosing an answer, you didn't receive the points I feel you deserve with this answer. I'm trying to find out how to still award the points on meta.stackoverflow.com. One way or the other you'll get your points. Thanks for the elaborate, useful answer. – Decent Dabbler Jan 31 '11 at 17:30
  • @arcain: I think I just upvoted about 15 of your answers (the majority of the ones that were chosen best answer). Not the most fancy solution, but it did the job of getting you the points. :) – Decent Dabbler Feb 01 '11 at 22:08
  • @fireeyedboy - Looks like your upvotes were rolled back by an anti-fraud script; too many upvotes by the same user in a short timeframe. I appreciate the effort, though. Thanks. – arcain Feb 02 '11 at 04:32
  • @arcain: that sucked! :-/ You know what I'm gonna do? I'm reopening with a bounty of `150`. And award it to you afterwards. That, is unless somebody comes up with even a better answer. ;-) – Decent Dabbler Feb 13 '11 at 13:12