I do GWT client side validation and I've a problem of how to show validation errors which are returned by validator. I debugged it and I can see that the set contains errors but driver doesn't show them. SimpleBeanEditorDriver is used.
Entry Entry = driver.flush();
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<Entry>> violations = validator.validate(Entry, Default.class);
if (violations.size() > 0) {
driver.setConstraintViolations(new ArrayList<ConstraintViolation<?>>(violations));
...
}
Tested on GWT ver. 2.4 and 2.5
The code is written according to https://developers.google.com/web-toolkit/doc/latest/DevGuideValidation but they're not using editors.
Does anybody make it work together GWT validation and Editors ? May be somebody can give links to good examples of it ? I couldn't find any working ones. Any help are welcomed!
Here is a simple example of how we are using editors/HasEditorError and ConstraintViolations. I have also included a sample from our ValueBoxEditorDecorator which allows us to layout error message.
Our activity
#Override
public void onSave() {
RequestFactoryEditorDriver<DashboardModelProxy, ?> driver = display.getDriver();
RequestContext context = driver.flush();
context.fire(new Receiver<Void>() {
#Override
public void onSuccess(Void response) {
Place previousPlace = clientFactory.getPlaceController().getPreviousPlace();
clientFactory.getPlaceController().goTo(previousPlace);
}
#Override
public void onFailure(ServerFailure error) {
display.showError(error.getMessage());
}
#Override
public void onConstraintViolation(Set<ConstraintViolation<?>> violations) {
display.getDriver().setConstraintViolations(violations);
}
});
}
Sample from our view.
/**
* Name component for the name of the analytics operation.
* This also implements {#link HasEditorErrors so it can show
* constraint violations when an error occurs.
*/
#UiField
ValueBoxEditorDecorator<String> name;
UIBinder example using the error location.
<t:ValueBoxEditorDecorator errorLocation="RIGHT" ui:field="name">
<t:valuebox>
<g:TextBox />
</t:valuebox>
</t:ValueBoxEditorDecorator>
The ValueBoxEditorDecorator we are using.
import java.util.List;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.editor.client.EditorError;
import com.google.gwt.editor.client.HasEditorErrors;
import com.google.gwt.editor.client.IsEditor;
import com.google.gwt.editor.client.adapters.TakesValueEditor;
import com.google.gwt.editor.ui.client.adapters.ValueBoxEditor;
import com.google.gwt.uibinder.client.UiChild;
import com.google.gwt.uibinder.client.UiConstructor;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.ValueBoxBase;
import com.google.gwt.user.client.ui.ValueListBox;
/**
* This is a copy of the original ValueBoxEditorDecorator in the gwt source The
* reason we are not using it is because it did not support laying out the error
* panel in a different location.
*
*
* A simple decorator to display leaf widgets with an error message.
* <p>
* <h3>Use in UiBinder Templates</h3>
* <p>
* The decorator may have exactly one ValueBoxBase added though an
* <code><e:valuebox></code> child tag.
* <p>
* For example:
*
* <pre>
* #UiField
* ValueBoxEditorDecorator<String> name;
* </pre>
*
* <pre>
* <e:ValueBoxEditorDecorator ui:field='name'>
* <e:valuebox>
* <g:TextBox />
* </e:valuebox>
* </e:ValueBoxEditorDecorator>
* </pre>
*
* #param <T>
* the type of data being edited
*/
public class ValueListBoxEditorDecorator<T> extends Composite implements HasEditorErrors<T>, IsEditor<TakesValueEditor<T>> {
/**
* The location of the text relative to the paging buttons.
*/
public static enum ErrorPanelLocation {
LEFT, RIGHT;
}
SimplePanel contents = new SimplePanel();
#Ignore
Label errorLabel = new Label();
HorizontalPanel layout = new HorizontalPanel();
private TakesValueEditor<T> editor;
/**
* Constructs a ValueBoxEditorDecorator.
*/
#UiConstructor
public ValueListBoxEditorDecorator(ErrorPanelLocation errorLocation) {
initWidget(layout);
setStyleName("gwt-ValueBoxEditorDecorator");
errorLabel.setStyleName("gwt-ValueBoxEditorDecorator-error");
errorLabel.getElement().getStyle().setDisplay(Display.NONE);
if (errorLocation == ErrorPanelLocation.RIGHT) {
layout.add(contents);
layout.add(errorLabel);
} else {
layout.add(errorLabel);
layout.add(contents);
}
}
/**
* Constructs a ValueBoxEditorDecorator using a {#link ValueBoxBase} widget
* and a {#link ValueBoxEditor} editor.
*
* #param widget
* the widget
* #param editor
* the editor
*/
public ValueListBoxEditorDecorator(ValueListBox<T> widget, TakesValueEditor<T> editor) {
this(ErrorPanelLocation.RIGHT);
contents.add(widget);
this.editor = editor;
}
/**
* Returns the associated {#link ValueBoxEditor}.
*
* #return a {#link ValueBoxEditor} instance
* #see #setEditor(ValueBoxEditor)
*/
public TakesValueEditor<T> asEditor() {
return editor;
}
/**
* Sets the associated {#link ValueBoxEditor}.
*
* #param editor
* a {#link ValueBoxEditor} instance
* #see #asEditor()
*/
public void setEditor(ValueBoxEditor<T> editor) {
this.editor = editor;
}
/**
* Set the widget that the EditorPanel will display. This method will
* automatically call {#link #setEditor}.
*
* #param widget
* a {#link ValueBoxBase} widget
*/
#UiChild(limit = 1, tagname = "valuebox")
public void setValueBox(ValueBoxBase<T> widget) {
contents.add(widget);
setEditor(widget.asEditor());
}
public void clearErrors() {
errorLabel.setText("");
errorLabel.getElement().getStyle().setDisplay(Display.NONE);
}
/**
* The default implementation will display, but not consume, received errors
* whose {#link EditorError#getEditor() getEditor()} method returns the
* Editor passed into {#link #setEditor}.
*
* #param errors
* a List of {#link EditorError} instances
*/
public void showErrors(List<EditorError> errors) {
StringBuilder sb = new StringBuilder();
for (EditorError error : errors) {
if (error.getEditor().equals(editor)) {
sb.append("\n").append(error.getMessage());
}
}
if (sb.length() == 0) {
clearErrors();
return;
}
errorLabel.setText(sb.substring(1));
errorLabel.getElement().getStyle().setDisplay(Display.INLINE_BLOCK);
}
}
This wiki might help you:
https://github.com/apetrelli/gwt-integration/wiki/GWT-Integration-Editor
although it integrates Editor, Validator and RequestFactory.
I created a Maven archetype that uses it:
https://github.com/apetrelli/samplegwt
Related
I'm having an issue that I thought would be VERY simple to accomplish. I can not get this very basic request validation to work. I can enter "Bob" on the create form and the edit form and get no error messages. It simply inserts into the database.
Here's my code. I feel I'm doing/not doing something stupid.
UserCrudController.php
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Requests\UserRequest;
use Backpack\CRUD\app\Http\Controllers\CrudController;
use Backpack\CRUD\app\Library\CrudPanel\CrudPanelFacade as CRUD;
/**
* Class UserCrudController
* #package App\Http\Controllers\Admin
* #property-read \Backpack\CRUD\app\Library\CrudPanel\CrudPanel $crud
*/
class UserCrudController extends CrudController
{
use \Backpack\CRUD\app\Http\Controllers\Operations\ListOperation;
use \Backpack\CRUD\app\Http\Controllers\Operations\CreateOperation;
use \Backpack\CRUD\app\Http\Controllers\Operations\UpdateOperation;
use \Backpack\CRUD\app\Http\Controllers\Operations\DeleteOperation;
use \Backpack\CRUD\app\Http\Controllers\Operations\ShowOperation;
/**
* Configure the CrudPanel object. Apply settings to all operations.
*
* #return void
*/
public function setup()
{
CRUD::setModel(\App\Models\User::class);
CRUD::setRoute(config('backpack.base.route_prefix') . '/user');
CRUD::setEntityNameStrings('user', 'users');
}
/**
* Define what happens when the List operation is loaded.
*
* #see https://backpackforlaravel.com/docs/crud-operation-list-entries
* #return void
*/
protected function setupListOperation()
{
CRUD::column('name');
CRUD::column('email');
//CRUD::column('password');
/**
* Columns can be defined using the fluent syntax or array syntax:
* - CRUD::column('price')->type('number');
* - CRUD::addColumn(['name' => 'price', 'type' => 'number']);
*/
}
/**
* Define what happens when the Create operation is loaded.
*
* #see https://backpackforlaravel.com/docs/crud-operation-create
* #return void
*/
protected function setupCreateOperation()
{
CRUD::setValidation(UserRequest::class);
CRUD::field('name');
CRUD::field('email');
//CRUD::field('password');
/**
* Fields can be defined using the fluent syntax or array syntax:
* - CRUD::field('price')->type('number');
* - CRUD::addField(['name' => 'price', 'type' => 'number']));
*/
}
/**
* Define what happens when the Update operation is loaded.
*
* #see https://backpackforlaravel.com/docs/crud-operation-update
* #return void
*/
protected function setupUpdateOperation()
{
$this->setupCreateOperation();
}
}
UserRequest.php
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
// only allow updates if the user is logged in
return backpack_auth()->check();
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'name' => ['required','min:5','max:255'],
];
}
/**
* Get the validation attributes that apply to the request.
*
* #return array
*/
public function attributes()
{
return [
//
];
}
/**
* Get the validation messages that apply to the request.
*
* #return array
*/
public function messages()
{
return [
//
];
}
}
I think what is happening is that you are not loading that controller.
If you have installed PermissionManager and now want to configure the UserCrudController you need to either manually register the routes yourself and point to your new controller, or alternativelly (and probably recommended) bind your new controller to the package one so your controller gets "served" instead of the package controller.
// In AppServiceProvider.php or any other provider of your choice:
$this->app->bind(
\Backpack\PermissionManager\app\Http\Controllers\UserCrudController::class,
\App\Http\Controllers\Admin\UserCrudController::class
);
If you don't need to change everything in the Controller you can directly extend the package UserCrudController and only change the things you need.
<?php
namespace App\Http\Controllers\Admin;
class UserCrudController extends \Backpack\PermissionManager\app\Http\Controllers\UserCrudController
{
}
The package controller already extends the CrudController.
The validation is added on setupCreateOperation() that you can override to fit your needs.
Cheers
I'm using FOSRestBundle in a Symfony project. When it I try to handle a view, it fails during the serialization of my data with the Symfony serializer as well as with the JMSSerializer.
This is the method rendering the response:
DefaultController.php
$em = $this->getDoctrine()->getManager('magellan');
$qb = $em->createQueryBuilder();
$query = $qb->select('h')
->from('DataBundle:Holding', 'h')
->where($qb->expr()->eq('h.id', ':holding_id'))
->setParameter('holding_id', $holding_id)
->getQuery();
$results = $query->getResult();
$view = $this->view($results, 200);
// Everything's ok up to this point
return $this->handleview($view);
And these are my entities:
Holding.php
class Holding
{
...
/**
* #ORM\OneToMany(targetEntity="Subsidiary", mappedBy="holding")
*/
private $subsidiaries;
}
Subsidiary.php
class Subsidiary
{
...
/**
* #ORM\ManyToOne(targetEntity="Holding", inversedBy="subsidiaries")
* #ORM\JoinColumn(name="id_holding", referencedColumnName="id_holding")
*/
private $holding;
/**
* #ORM\OneToMany(targetEntity="Brand", mappedBy="subsidiary")
*/
private $brands;
}
Brand.php
class Brand
{
...
/**
* #ORM\ManyToOne(targetEntity="Subsidiary", inversedBy="brands")
* #ORM\JoinColumn(name="id_subsidiary", referencedColumnName="id_subsidiary")
*/
private $subsidiary;
/**
* #ORM\OneToMany(targetEntity="Product", mappedBy="brand")
*/
private $products;
}
Product.php
class Product
{
...
/**
* #ORM\ManyToOne(targetEntity="Brand", inversedBy="products")
* #ORM\JoinColumn(name="id_brand", referencedColumnName="id_brand")
*/
private $brand;
/**
* #ORM\ManyToOne(targetEntity="Sector", inversedBy="products")
* #ORM\JoinColumn(name="id_sector", referencedColumnName="id_sector")
*/
private $sector;
/**
* #ORM\OneToMany(targetEntity="Commercial", mappedBy="product")
*/
private $commercials;
}
Commercial.php
class Commercial
{
...
/**
* #ORM\ManyToOne(targetEntity="Product", inversedBy="commercials")
* #ORM\JoinColumn(name="id_product", referencedColumnName="id_product")
*/
private $product;
/**
* #ORM\OneToMany(targetEntity="CommercialReport", mappedBy="commercial")
*/
private $reports;
CommercialReport.php
class CommercialReport
{
...
/**
* #ORM\ManyToOne(targetEntity="Commercial", inversedBy="reports")
* #ORM\JoinColumn(name="id_commercial", referencedColumnName="id_commercial")
*/
private $commercial;
}
Sector.php
class Sector
{
...
/**
* #ORM\OneToMany(targetEntity="Product", mappedBy="sector")
*/
private $products;
}
When using the default symfony serializer, I get the following error:
"message":"A circular reference has been detected (configured limit:
1).","class":"Symfony\Component\Serializer\Exception\CircularReferenceException"
And when using the JMSSerializer, when I go to the corresponding page of the controller, the page just never finishes loading. At the same time in the dev.log file new Doctrine.debug entries with requests to my DB are added every second.
$normalizers->setCircularReferenceHandler(function ($object) {
return $object->getId();
});
Just add it after you make the instance if your objectNormalizer()
it worl perfectly for me
If you use FosRestBundle, you can use the GROUPS for the serializer. There is an annotation given by FosRestBundle : #FOS\RestBundle\Controller\Annotations\View(serializerGroups={"user"})
Your group can exclude the circular property.
Another idea you can do this. In your app/config/services.yml
circular_reference_handler:
public: false
class: callback
factory: [AppBundle\Serializer\CircularHandlerFactory, getId]
serializer.normalizer.object:
class: Symfony\Component\Serializer\Normalizer\ObjectNormalizer
arguments: ["#serializer.mapping.class_metadata_factory", null, "#serializer.property_accessor"]
public: false
tags: [serializer.normalizer]
calls:
- method: setCircularReferenceLimit
arguments: [1]
- method: setCircularReferenceHandler
arguments: ["#circular_reference_handler"]
The factory can be like this:
namespace AppBundle\Serializer;
class CircularHandlerFactory
{
/**
* #return \Closure
*/
public static function getId()
{
return function ($object) {
return $object->getId();
};
}
}
I develop the GWT application. Also I use Twitter Bootstrap library and GWTQuery. There is the DropdownButton. I want to open it programmatically.
ui.xml like this:
<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
xmlns:b="urn:import:com.github.gwtbootstrap.client.ui"
xmlns:g='urn:import:com.google.gwt.user.client.ui'
...
<b:DropdownButton text="Test" ui:field="dropdownButton">
<g:FlowPanel ui:field="contentPanel"/>
</b:DropdownButton>
I can open it via inspector of Google Chrome - add 'open' class to 'btn-group'. But it is impossible programmatically. I don't know why. Usages of addClassName / addStyleName methods are ignored.
Also I have tried to simulate click event via Document.get().createClickEvent, but the dropdown hasn't handlerManager. I have tried to call trigger and click with help of JQuery/GWTQuery.
Is it possible to open Dropdown programmatically?
You can try subclassing DropDownButton, adding a getter for the trigger button (at the end of the class), like this:
public class CustomDropdownButton extends DropdownBase {
private Button trigger;
/**
* Creates a DropdownButton without a caption.
*/
public CustomDropdownButton() {
super("div");
addStyleName("btn-group");
}
/**
* Creates a DropdownButton with the given caption.
*
* #param caption
* the button's caption
*/
public CustomDropdownButton(String caption) {
this();
setText(caption);
}
/**
* {#inheritDoc}
*/
#Override
protected IconAnchor createTrigger() {
trigger = new Button();
trigger.setCaret(true);
return trigger;
}
/**
* Sets the button's size.
*
* #param size
* the button's size
*/
public void setSize(ButtonSize size) {
trigger.setSize(size);
}
/**
* Sets the button's type.
*
* #param type
* the button's type
*/
public void setType(ButtonType type) {
trigger.setType(type);
}
/**
* Sets the button's icon.
*
* #param type
* the icon's type
*/
#Override
public void setIcon(IconType type) {
setBaseIcon(type);
}
/**
* {#inheritDoc}
*/
#Override
public void setBaseIcon(BaseIconType type) {
trigger.setBaseIcon(type);
}
#Override
public HandlerRegistration addClickHandler(ClickHandler handler) {
return trigger.addClickHandler(handler);
}
/**
* {#inheritDoc}
*/
#Override
public void setIconSize(IconSize size) {
trigger.setIconSize(size);
}
/**
* {#inheritDoc}
*/
#Override
public void setCustomIconStyle(String customIconStyle) {
trigger.setCustomIconStyle(customIconStyle);
}
/**
* {#inheritDoc}
*/
#Override
public void setIconPosition(IconPosition position) {
trigger.setIconPosition(position);
}
public Button getButton(){
return trigger;
}
}
Now you will have in your uiBinder xml :
<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
xmlns:b="urn:import:com.github.gwtbootstrap.client.ui"
xmlns:g='urn:import:com.google.gwt.user.client.ui'
xmlns:c='urn:import:com.example.packageWithCustomDropDown'
...
<c:CustomDropdownButton text="Test" ui:field="dropdownButton">
<g:FlowPanel ui:field="contentPanel"/>
</b:CustomDropdownButton>
And now you can call, the click function, on the button : dropDownButton.getButton().click();
I don't test it, but it should work.
Hope it helps. :)
I traced it down to the getDatastoreClass returning a null datastore class to the createPerImplementationColumnsForReferenceField.
I have tried both the 3.1.1 and now using 3.2.0-m4 release hoping that would fix my problem.
RDBMSStoreManager#getDatastoreClass(String className, ClassLoaderResolver clr);
It is returning a null datastore class to the
ReferenceMapping#createPerImplementationColumnsForReferenceField(boolean pk, boolean nullable, boolean serialised, boolean embedded, int fieldRole, ColumnMetaData[] columnMetaData, ClassLoaderResolver clr)
I am using the orm to annotate a mapped-superclass and this mapped-superclass does not have a table definition and both of my mapped-superclass throwing this exception.
499170 [http-bio-8080-exec-8] DEBUG DataNucleus.Datastore.Schema - Field [com.hp.vf.server.domain.AlertDefinition.isPublic] -> Column(s) ["ALERTDEFINITION"."ISPUBLIC"] using mapping of type "org.datanucleus.store.rdbms.mapping.java.BooleanMapping" (org.datanucleus.store.rdbms.mapping.datastore.SmallIntRDBMSMapping)
551964 [http-bio-8080-exec-8] DEBUG DataNucleus.Persistence - Managing Persistence of Class : com.hp.vf.analytics.shared.metric.Metric [Table : (none), InheritanceStrategy : subclass-table]
561964 [http-bio-8080-exec-8] ERROR DataNucleus.Datastore.Schema - An exception was thrown while adding/validating class(es) : null
java.lang.NullPointerException
at org.datanucleus.store.rdbms.mapping.java.ReferenceMapping.createPerImplementationColumnsForReferenceField(ReferenceMapping.java:452)
at org.datanucleus.store.rdbms.mapping.java.ReferenceMapping.prepareDatastoreMapping(ReferenceMapping.java:214)
at org.datanucleus.store.rdbms.mapping.java.ReferenceMapping.initialize(ReferenceMapping.java:110)
at org.datanucleus.store.rdbms.mapping.java.InterfaceMapping.initialize(InterfaceMapping.java:54)
In the Reference mapping, dc is null when trying to execute getIdMapping(), I have verified this in the debugger.
try
{
DatastoreClass dc = storeMgr.getDatastoreClass(implClass.getName(), clr);
m = dc.getIdMapping(); // DC is null
}
catch (NoTableManagedException ex)
{
// TODO Localise this message
throw new NucleusUserException("Cannot define columns for " + mmd.getFullFieldName() +
" due to " + ex.getMessage(), ex);
}
Here is the problematic class file.
public abstract class Metric implements IMetric {
/**
* Serialization ID
*/
private static final long serialVersionUID = 3806479436166940035L;
private Long id;
/**
* The name of the metric, this is not mandatory we have some metrics that
* may come back without names.
*/
protected String name;
/**
* This is an optional metric value that can be set by the script in order
* to add context to the execution of the metric.
*/
protected String context;
/**
* The list of violations associated with this metric.
*/
protected List<Violation> violations = null;
public Metric() {
violations = new ArrayList<Violation>();
}
/**
* Constructor that takes the name of the object and the value that it
* represents.
*
* #param name
* #param value
*/
public Metric(String name) {
this();
this.name = name;
}
/* (non-Javadoc)
* #see com.hp.vf.taskengine.shared.metric.IMetric#getName()
*/
#Override
public String getName() {
return name;
}
/* (non-Javadoc)
* #see com.hp.vf.taskengine.shared.metric.IMetric#setName(java.lang.String)
*/
#Override
public void setName(String name) {
this.name = name;
}
/**
* This is the context that represents the metric. This could have come from
* R and would be a Key:Value; pair of values used to calculate the value.
* For example if a metric was calculated for a product in houston for ISS
* the context may look like "ProdNum:1234;Factory:Houston;BUnit:ISS". This
* context is useful when chaining together tasks.
*
* #return String context used when chaining tasks together.
*/
public String getContext() {
return context;
}
/* (non-Javadoc)
* #see com.hp.vf.taskengine.shared.metric.IMetric#toString()
*/
public String toString() {
String debugString = "";
debugString += "Metric: " + name;
return debugString;
}
/* (non-Javadoc)
* #see com.hp.vf.taskengine.shared.metric.IMetric#hasMetricViolations()
*/
#Override
public boolean hasMetricViolations() {
return (violations != null && violations.size() > 0) ? true : false;
}
/* (non-Javadoc)
* #see com.hp.vf.taskengine.shared.metric.IMetric#getViolations()
*/
#Override
public List<IViolation> getViolations() {
return violations;
}
/* (non-Javadoc)
* #see com.hp.vf.taskengine.shared.metric.IMetric#setViolations(java.util.List)
*/
#Override
public void setViolations(List<IViolation> violations) {
this.violations = violations;
}
#Override
public Long getId() {
return id;
}
}
Here is an excert from my orm.xml file
<access>FIELD</access>
<mapped-superclass class="com.hp.vf.analytics.shared.metric.Metric" access="FIELD">
<attributes>
<basic name="name" />
<basic name="context" />
<one-to-many name="violations">
<cascade>
<cascade-all />
</cascade>
</one-to-many>
</attributes>
</mapped-superclass>
Am I doing something wrong or is this a bug?
I was able to resolve the problem using the tips from Datanucleus. I had multiple problems
Problems
The most obvious and silly on my part
If you have an one-to-many or one-to-one that uses an interface you must specify the target-entity.
/**
* The list of violations associated with this metric.
*/
protected List<IViolation> violations = null;
...
<one-to-many name="violations"
target-entity="com.vf.analytics.shared.metric.Violation">
<cascade>
<cascade-all />
</cascade>
</one-to-many>
Don't use generics, the jpa spec does not suppport them
Not sure if datanucleus supports generics but it but probably not recommended.
public MetricNumber< T extends Number> extends Metric implements IMetric {
T value;
}
If you extend an entity that is not annotated make sure that you add it to your orm.xml
I wasn't testing some of the objects so I assumed that datanucleus would ignore them but this appears not to be the case. I had extended third party classes that were not annotated but had not yet added them to the orm.xml, this was also causing null the null pointer exceptions I was getting.
Is it possible to have a class annotated with [DBus (name = ...)] implement an interface?
Following the example at https://live.gnome.org/Vala/DBusServerSample, I am implementing a D-BUS client/server application.
One thing that I found peculiar about the example was that there was no separate interface definition. I would like to have the interface used by the client side in a separate file, and have the server class implement that interface. That way I can have the compiler tell me when I miss something.
This does not appear to work with properties though. The following definition is compatible with what I have:
/* interface.vala */
namespace org.test {
[DBus (name = "org.test.Items")]
public interface IItems : Object {
/**
* The object paths to the item instances.
*
* These objects are of type org.test.items.Item.
*/
public abstract ObjectPath[] items {
owned get;
}
/**
* The signal that is emitted when a new item is added.
*
* When this signal is emitted, the item will be available.
*
* #param id
* The object path to the item instance.
*/
public signal void item_added(ObjectPath id);
/**
* The signal that is emitted when an item is removed.
*
* When this signal is emitted, the item will be unavailable.
*
* #param id
* The object path to the item instance.
*/
public signal void item_removed(ObjectPath id);
/**
* Adds a new item.
*
* The URL will be parsed, and if it contains a valid item, it will be
* added.
*
* #param url
* The URL to the item. This should typically be the URL of the
* RSS feed.
* #return the ID of the item added, which can be used to query D-BUS
* for it
* #throws IOError if a D-BUS error occurs
*/
public abstract ObjectPath add_item(string url) throws IOError;
/**
* Removes an item.
*
* #param id
* The ID of the item to remove.
* #throws IOError if a D-BUS error occurs
*/
public abstract void remove_item(ObjectPath id) throws IOError;
}
}
/* server.vala */
using Gee;
namespace org.test {
[DBus (name = "org.test.Items")]
public class Items : DBUSObject, IItems {
private ArrayList<Item> _items;
[DBus (visible = false)]
protected override void dbus_register(DBusConnection conn,
ObjectPath path) throws IOError {
conn.register_object(path, this);
}
[DBus (visible = false)]
public Items() {
base("org.test.Items", "/org/test", "Items", true);
_items = new ArrayList<Item>();
}
[DBus (visible = false)]
~Items() {
unregister();
}
/**
* #see interface.vala::org.test.IItems.comics
*/
public ObjectPath[] items {
owned get {
ObjectPath[] result = {};
foreach (var item in _items) {
result += new ObjectPath(item.path);
}
return result;
}
}
/**
* #see interface.vala::org.test.IItems.add_comic
*/
public ObjectPath add_item(string url) throws IOError {
/* . . . */
}
/**
* #see interface.vala::org.test.IItems.remove_item
*/
public void remove_item(ObjectPath id) throws IOError {
/* . . . */
}
}
}
When I compile it, I get no error from valac, but when the generated C code is compiled, the linker complains: undefined reference to 'org_test_items_get_items'.
This function is referenced by _dbus_org_test_items_get_items, but it does not exist
It's obviously a bug. The right place to report bugs is http://bugzilla.gnome.org .