
hakim.cassimally at gmail
Nov 12, 2009, 4:28 PM
Post #1 of 1
(441 views)
Permalink
|
Hi! I was chatting earlier with kentnl, f00li5h, mst, zamolxes, and later with rafl, t0m about a problem I'm trying to solve. I'll summarize, (possibly mainly to clear /my/ head a little... hope this makes any sense): The application I'm working on has various types of main content, for example: - a list of users - users in my team - users from an admin search - etc. - a list of quizzes - a single quiz - a list of questions - etc. These are all basically "domain model objects", in my case DBIC objects. Normally I'll want to display these as an HTML page, but I may want to be able to display, for example, a CSV file, or an RSS document. To make things nice and generic, I want to be able to pass the DBIC objects I've already retrieved and have them magically turned into a CSV file etc.! But it's more complicated than that. Perhaps a "list of users" will have different fields if they're "users in my team" than if I'm an admin and searched for them, for example. This knowledge doesn't really belong: - in the Model (which is a domain model, and shouldn't care about how it's presented!) - in the View (which shouldn't be hard-coded to show different things to different controllers) - in the Controller might be fine... but then we'd have to check which view we're in in every controller, which seems wrong too. I've understood that it's far more complicated to do this all( 'correctly', 'elegantly', 'extensibly', 'without being a huge pain in the arse', ) so I'm considering settling for all('working', 'not too much of a pain in the arse'). But I was wondering if the following code sketch has enough of value that anything can be saved from it? Comments welcome! =head2 The View This is a view that display Comma Separated Values. It expects $c->stash( fields => [qw/ list of field names /], rows => [ [ list, of, row, data ], [ list, of, row, data ], [ ... ], ], ); =cut package MyApp::Web::View::Tabular; use strict; use warnings; use base 'Catalyst::View'; use Text::CSV_XS; sub process { my ($self, $c) = @_; my $tabular = $c->stash->{tabular} or die "No tabular data! Did you pass through an adaptor?"; my $csv = Text::CSV_XS->new; my $string = join "\n", map { $csv->combine(@$_) or die "Error creating CSV " . $csv->error_input; $csv->string } $c->stash->{tabular}{fields}, @{ $c->stash->{tabular}{rows} }; $c->res->body( $string ); $c->res->content_type( 'text/plain' ); } 1; =head2 The Problem ...the Controller didn't generate the data above: it just created something like: $c->stash( items => [. bless { ... } MyApp::Model::Something, bless { ... } MyApp::Model::Something, bless { ... } MyApp::Model::Something, ] }; So I want to convert between the two. So... =head2 Controller end action forwards to a ViewModel! The following code is fugly, but the intention is that, for example: We're in controller 'Dashboard' and have objects of type 'Module' Then we'll check these components (in order): MyApp::Web::Model::View::Dashboard::Module MyApp::Web::Model::View::Dashboard MyApp::Web::Model::View::Module and dispatch to the first that exists, if any. =cut --- MyApp/Web/Controller/Root.pm sub end : ActionClass('RenderView') { my ($self, $c) = @_; + $c->forward('view_model') + if $c->view->use_view_model; } +sub view_model : Private { + my ($self, $c) = @_; + + # isn't there $c->view->**name or similar** to get this string? + my $view = $c->stash->{current_view} || $c->config->{default_view}; + my $view_model_prefix = "View::${view}"; + + my $model_class = $c->stash->{model_class}; + + my $controller = do { + my $class = $c->action->class; + my $prefix = (ref $c) . '::Controller::'; + $class=~s/^$prefix//; + $class; + }; + + my @possible_view_models = ( + $model_class ? "${view_model_prefix}::${controller}::$model_class" : (), + "${view_model_prefix}::${controller}", + $model_class ? "${view_model_prefix}::$model_class" : (), + ); + + if (my $view_model = first { $c->model($_) } @possible_view_models) { + $c->forward($view_model); + } + } =head2 ModelView knows how to turn into the right data format (Note I'm having to accept $c->stash to get and set this data) (oh, and $c itself, because the subclass needs it below) =cut package MyApp::Web::Model::View::Tabular; use Moose; with 'Catalyst::Component::InstancePerContext'; has _stash => ( isa => 'HashRef', is => 'rw', ); has _c => ( isa => 'MyApp::Web', is => 'rw', ); sub build_per_context_instance { my ($class, $c) = @_; return $class->new( _stash => $c->stash, _c => $c, ); } sub process { my ($self) = @_; my $items = $self->_stash->{items} or die "No items to display"; my $tabular = $self->get_table_definition( $self->_c ); my @fields = map $_->{field}, @$tabular; my @data_subs = map $_->{data}, @$tabular; my @rows = map { my $item = $_; [ map $_->($item), @data_subs ]; } @$items; $self->_stash->{tabular} = { fields => \@fields, rows => \@rows, }; } sub get_table_definition { die "Abstract method get_table_definition called! Please override in subclass."; } 1; =head2 The Subclass then defines how the data will be transformed =cut package MyApp::Web::Model::View::Tabular::Dashboard; use strict; use parent 'MyApp::Web::Model::View::Tabular'; sub get_table_definition { my ($self, $c) = @_; return [. { field => 'Name', data => sub { shift->module->name } }, { field => 'URL', data => sub { $c->uri_for('/module/take', shift->module->id) } }, { field => 'Description', data => sub { shift->module->short_desc } }, { field => 'Due Date', data => sub { shift->due_date } }, { field => 'Category', data => sub { shift->module->category->id } }, { field => 'Duration', data => sub { shift->module->duration } }, ]; } 1; =head2 Querying the configuration We mentioned $c->view->use_view_model above, so let's create it! It seems to make sense to have this configurable, but even if we add to config, we have no way of querying it! So we create a role, to expose this configuration to the public. =cut package MyApp::Web::Role::View::QueryConfig; use Moose::Role; # expose parts of config that all views should expose has use_view_model => ( isa => 'Int', is => 'ro', ); 1; --- MyApp/Web.pm +use MyApp::Web::Role::View::QueryConfig; __PACKAGE__->config( + 'View::Tabular' => { + use_view_model => 1, + }, ); # crack, via t0m +# ensure that all ::View's can be queried for use_view_model +before setup_component => sub { + my ($app, $comp) = @_; + MyApp::Web::Role::View::QueryConfig->meta->apply($comp->meta) + if $comp->isa('Catalyst::View'); + }; + # Start the application __PACKAGE__->setup(); # Cheers, osfameron _______________________________________________ List: Catalyst [at] lists Listinfo: http://lists.scsys.co.uk/cgi-bin/mailman/listinfo/catalyst Searchable archive: http://www.mail-archive.com/catalyst [at] lists/ Dev site: http://dev.catalyst.perl.org/
|