Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

How to specify the viewModel for relations? #28

Closed
SystemParadox opened this Issue · 9 comments

3 participants

@SystemParadox

Hi. I'm having trouble specifying which viewModel knockback should use for related models.

At the moment I'm dealing with a 'HasOne' relation. The relation is working fine, and knockback is creating an ordinary kb.ViewModel for the related model.

However, if I specify:

options.children.foo = fooViewModel

It uses a fooViewModel for the related item, but it creates it directly instead of using a kb.viewModelAttributeConnector. This means that it works as expected if the related item has already been retrieved, but where it has not been retrieved yet, the lack of the connector layer means that the related item doesn't get updated.

This appears to be in the handling for AttributeConnector.createOrUpdate. Ordinarily it calls createByType to make a viewModelAttributeConnector, passing along the options. Adding the view_model to the options should give the desired behaviour as createByType handles this correctly. However, createOrUpdate intercepts the view_model option- if it finds one, it creates the viewModel directly without the connector. I cannot find any way of bypassing this and getting the view_model option into createByType.

Any hints?

Thanks.
Simon

@kmalakoff
Owner

@SystemParadox Hi Simon. It is on my list to properly test and review the API before making making it officially public since it hasn't been through a rigorous testing process and is subject to change. (think of it as an undocumented, pre-alpha API).

That said, while I was developing it, I tested certain cases. Please take a look at https://github.com/kmalakoff/knockback/blob/master/test/knockback_view_model/test.coffee under the "Collection with nested custom view models" tests to get a feeling for the way the API is supposed to work (even if it can be improved!):

    nested_view_model = kb.viewModel(nested_model, {
      children:
        john: ContactViewModelDate
        paul: {view_model: ContactViewModelDate}
        george: {view_model_create: (model) -> return new ContactViewModelDate(model)}
        george2: {create: (model, key) -> return new ContactViewModelDate(model.get(key))}
        major_duo: {children: ContactViewModelDate}
        major_duo2: {children: {view_model: ContactViewModelDate}}
        major_duo3: {children: {view_model_create: (model) -> return new ContactViewModelDate(model)}}
        minor_duo2: {children: {view_model: kb.ViewModel}}
        minor_duo3: {children: {create: (model) -> return new kb.ViewModel(model)}}
    })

That said, this may not give you the fine grained control you need...

The tests and API I didn't get finished with were based on more customized cases when I was writing knockback-inspector (which I didn't end up needing to use). These were my samples in-progress where I tried to play around with overriding the default behavior.

    options.children =
      create: (model, key, options) ->
        type = kb.AttributeConnector.inferType(model, key)

        # override the view model types
        return new ModelInspectorViewModel(model.get(key), options) if (type == 'model')
        options = {view_model: ModelInspectorViewModel, __kb_store: options.__kb_store} if (type == 'collection')

        return kb.AttributeConnector.createByType(type, model, key, options)

and

Model
  constructor: (model, options={}) ->

    # register ourselves to handle recursive view models
    kb.Store.resolveFromOptions(options, this)
    options.store_skip_resolve = true

    kb.utils.optionsCreateOverride(options, {children: {create: ModelNode.create}})
    @model = kb.viewModel(model, options)

  @create: (model, key, options) ->
    type = kb.AttributeConnector.inferType(model, key)

    # override the view model types
    switch type
      when 'model'
        return new ModelNode(model.get(key), options, key, false)
      when 'collection'
        return new CollectionNode(model.get(key), options, key, true)
      else
        return kb.AttributeConnector.createOrUpdate(null, model, key, options)

I'm not sure if this is useful. What I recommend is try to understand these three code snippets, write the API like it "should" be like and I'll see if I can make it that way.

Kevin

@SystemParadox

Thanks for getting back to me so quickly. I've been playing around with this a bit more and at the moment I've commented out all the code in AttributeConnector.createOrUpdate that checks create, view_model, etc so that it delegates to createByType. This seems to be working for me, although as you'd probably expect it's making many tests fail. Whether anything else depends on the current behaviour or not I don't know.

Please consider the following snippet in the "Collection with nested custom view models" tests:

validateContactViewModel(nested_view_model.george2, 'George', george_birthdate)
validateGenericViewModel(nested_view_model.ringo(), 'Ringo', ringo_birthdate)

Notice that 'ringo' (no custom options) is called as a function whereas 'george2' (custom view_model_create given) is not. It would seem that this inconsistency is the source of my issue. Is it possible to change it so that a function call is always used (that is, it always gets wrapped in a kb.viewModelAttributeConnector)?

If I get chance I'll look into actually modifying some tests, but I'm a bit out of my depth here at the moment.

Many thanks.

@kmalakoff
Owner

Hi Simon,

I really appreciate you taking the time to work through this. I'm sorry about taking a few days to get back to you, but just finished a big project deadline.

So if I understand correctly, the problem arises because of the inconsistency between a custom property for a Backbone.Model (not wrapped in a ko.observable) and an automatically generated kb.ViewModel-type property for a Backbone.Model (wrapped in a ko.observable).

I remember when I wrote this code, I debated a lot about consistency and making things self-evident. I wasn't happy to wrap the ViewModel properties in ko.observables, but I thought if the attributes changed type, I would be requested to implement correct handling type-change in the future. As I mentioned in your other thread, this code is ready for the future in the sense that (even though it is currently not implemented, when an attribute changes type, Knockback can change the attribute's kb.ViewModel, kb.CollectionObservable or simple attribute behind-the scenes.

I suppose the question is should Knockback handle attributes changing type?

For example, maybe Knockback should only assume that attributes are the same type even if their values change. That means we would get rid of the ko.observable wrapping around the ViewModel implementations. In a kb.ViewModel, if the underlying attribute changes:

1) collections -> the property type would be kb.CollectionObservable (not wrapped in a ko.observable) and it can be modified in two ways: if the collection changes, it would call vm.collection_property.collection(new_collection) AND if the contained models are modified, it modifies them using the Backbone.Collection events.
2) models -> the property type would be a kind of ViewModel instance (not wrapped in a ko.observable) and it can be modified in two ways: if the model changes, it would call vm.model_property.model(new_model) AND if the contained attributes are modified, it modifies them using the Backbone.Model events.
3) Simple attributes -> simply set through a vm.attribute_property(new_value)

OR should we wrap all view model properties assuming attribute types can change (I just realized there is an inconsistency with simple attribute type changing which would require knockback to wrap all of the attributes with ko.observables in case their type changes):

1, 2, 3) same as above, but wrapped in a ko.observable in case the underlying attribute type changes.

I am kind of leaning towards assuming attribute types don't change after the ViewModel is created and getting rid of the extra wrapping. The reason for this is that perhaps this use case is unlikely to happen, (for consistency) it would force simple attribute types to also be wrapped in ko.observables which would be inconsistent with most-people's own custom view models, and just the KISS principle at work.

In that way, the inconsistent code would go the other way where ringo would lose the function wrapper:

validateContactViewModel(nested_view_model.george2, 'George', george_birthdate)
validateGenericViewModel(nested_view_model.ringo, 'Ringo', ringo_birthdate)

What do you think? KISS or changing attribute types after creating the view model is a case Knockback should handle?

Cheers,

Kevin

@kmalakoff
Owner

@SystemParadox : I just thought I'd check in to test an idea with you...

The main problem that I am trying to solve with the wrapping of view models and collections is to not change the property on the view model since Knockout requires the observables to be the same in order to track dependencies using their implicit system.

The edge cases where this can be a problem are:

1) changing the type of a Model attribute. Actually, the problem is switching between simple, model, or collection types since things being observed are not the same: simple (ko.observable), model (javascript object with properties for simple, model or collection types), collection (ko.observableArray)

2) having a null model that later is set to a model. It can be impossible to know what type the null will eventually become to set the view model property correctly (that said, in the special case of a RelationalModel, this can be checked, but I prefer to handle the general case for people not using Backbone-Relational).

I was thinking that because these cases are rare, edge-cases that I try to keep things simple for the simple case. Proposal:

1) Make two modes for the kb.ViewModel. One mode is static (eg. types don't change so view model properties are not wrapped in ko.observables) and the other is dynamic (eg. attribute types are permitted to change -> so all view model properties get wrapped in a ko.observable)

2) Provide an option for the static mode like "type_signatures" (which would be a javascript object mapping attribute names to types). It only needs to be provided if you know there will be model-type or collection-type attributes that can be null when the kb.ViewModel is initialized. If you know all your attributes will be instantiated with the correct types at view model create time, then "type_signatures" can be ignored.

What do you think about this proposal? Would it be a good answer to your consistency problem? (Personally, I believe the static option would be great for simplifying the syntax in using kb.ViewModels with Models and Collections since they wouldn't need to be wrapped in ko.observables, which I find to be a hard-to-remember syntax).

Any advice or ideas would be much appreciated!

Kevin

@SystemParadox

I'm not entirely clear on the type-changing issue. I've been trying to think of examples, but I can't see why it would be necessary. Type-changing would fix the null-model issue, but maybe it would be better to fix the null-model issue by itself and not do type-changing?

I would suggest that the rule should be 'everything is observable'. Since simple types are ko.observable already and collections are ko.observableArray already, it would seem unnecessary to wrap those in an extra layer. If models are just javascript objects I would suggest that these should be wrapped in an observable.

I still wonder what the best-practice is for creating kb viewmodels. For the purposes of this discussion I am going to define the following terms:

  • view: a page-specific knockout view-model (will likely make use of one or more models)
  • viewmodel: a view-model for looking at a model, constructed using knockback.
  • model: a backbone model, reusable across all pages

  1. Who is responsible for defining the viewmodels? At the moment I have view-models defined on a per-model basis (e.g. a PersonModel has a PersonViewModel that is shared across all pages), but I guess that it could also be done on a per-view basis (with each view defining its own simple viewmodels as necessary).
  2. When I started out I considered a viewmodel instance to be specific to a model instance and thus I was re-creating the viewmodel whenever the model changed. It would seem that the knockback way is to think of a complete view/viewmodel structure where only the data changes and the structure remains fixed (this is where the issue of null-models comes in- if the structure is fixed, I have to be able to create a blank view-model). I wonder what the most intuitive method would be. Maybe it would be worthwhile to explicitly define and explain this principle in the documentation.

I hope that is helpful!

Thanks.
Simon

@kmalakoff
Owner

Simon, I really appreciate your feedback and ideas!

I think you are right about type-changing. I had one case before where my app synchronized with a couchdb changes stream and a model attribute type could change (changing the model for scheduling information), but there are other ways to do that without changing the underlying types.

Convinced. I'll try out wrapping view models in observables even when they are created by the user in overrides functions.

Any ideas on how best to handle the null case? In inferType, I use the Backbone.RelationalModel getRelations() function but I think the signature is a little too specialized for relations to look for the signature as the general solution. So I guess "type_signatures" could be a solution:

class MyModel extends Backbone.Model
{
  type_signatures:
    some_attribute_name: ViewModelType
}

This would only be needed for non-Backbone-Relational relations and only when a null attribute needs to be mapped to a view model type to be pre-defined for any bindings. Any better ideas?

On the relationship between models and view model, I had a small discussion on the Knockback website (http://kmalakoff.github.com/knockback/docs.html under "Note on Relationships between Models and ViewModels - Often One-To-Many"). Maybe I can improve it and raise its profile as you suggest by putting in in a tutorial.

If I was to expand on recommendations, this is what I would say:

1) ViewModels should be tailored to the view they are used in because ko.observables can be heavy. What this means is that kb.ViewModel is good for rapid prototyping, but personally, I use hand-crafted ViewModels optimized to View they are being bound to (One-To-Many) since ViewModels are basically Controllers (or tightly coupled with Controller logic).

2) For ownership, I use an MVC paradigm where the controller generates and owns the ViewModels. Based on 1) and the principles of MVC/MVVM, I purposefully did not make a tight coupling between Models and ViewModels (despite some suggestions to use inheritance to link them) so typically a light Controller is needed which defines the fixed structure (your point 2). Because Backbone comes with a reasonable routing solution, but with Knockout, Backbone.View become obsolete (besides View being an ill-named Controller), I adapted a Brunch setup on my most recent example code:

I haven't really looked deeply into optimizing the brunch way of doing things (since seems is a little heavy), but it seems like a good example for how to handle ownership.

I can add a better explanation for the principle about Knockback using the fixed structure ViewModel from Knockout where Model changes are synced behind the scenes and I can write up a reference architecture (using brunch as a starting point). I still have your model setting example on my todo list, but I've been working freelance recently and it has been taking up too much of my spare time. I'll get to it soon.

Very helpful! Please share your thoughts any time!

Kevin

@kmalakoff
Owner

@SystemParadox : Hi Simon, I am sorry it took me so long to get back to this.

I have created a simplified version of the create and view model functions in 0.16.0beta1 (master).

You can see an example here: #34 (comment)

Also, I made all view models wrapped when created in a kb.ViewModel for consistency as you suggested.

I'll get to all of your documentation suggestions soon. For now, if you have a few moments, please take a look at 0.16.0beta1 and let me know what you think.

Cheers,

Kevin

@kmalakoff
Owner

@SystemParadox : I've updated the website using a lot of your feedback and have created Codo documentation. Thank you for your suggestions!

@kmalakoff kmalakoff closed this
@alanbarber111

Hey Kevin,

Hope this isn't tl;dr

Just now finding this thread. I was curious if you could expand more on one of the points you made --- mainly being the one-to-many viewmodel--model relationship.

I just want to make sure I'm understanding correctly. I'm trying to use my view models like specialized controllers.

So for example, I have a few pages:

  • My dashboard
  • My profile
  • Login
  • Logout

All of these are linked to a "user session" model. The logic for these 4 actions is encapsulated in one user session view model.

Are you suggesting that I should have a separate view model for each of these actions? Or rather that the separation should be similar to how one might define a controller in rails or zend --- ie, being a group of related actions can all be under one roof.

e.g., if I'm building a webstore and I have "products" I'll have a different view model for the portion of the application involved with editing the products than I would from the portion that is involved in selling them to the user --- but not necessarily for each step in the editing or selling process. You mentioned ko.observables being rather heavy --- but I'd imagine as long as they're being released on a page change, the browser can easily accommodate almost any view model?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.