This is a migrated thread and some comments may be shown as answers.

Example using generics...

11 Answers 293 Views
General Discussions
This is a migrated thread and some comments may be shown as answers.
This question is locked. New answers and comments are not allowed.
MWM
Top achievements
Rank 1
MWM asked on 08 Jan 2009, 11:08 PM
I've been successful in enabling my project for OA.  I've also handled using a scope in a single page to read/write data using OA.

What I'd like to do now is get an example class to be used as the base class for what's generated by OA using generics.  I'd like to have a single "GetbyID" method in this class that when passed a certain object type, it would search by ID and return an object of same.  Similary, I'd like other methods in this generic class to handle queries of something to gather multiple records (objects) which returns a collection based on a relation. 

Keep it simple, but useful... does anyone have anything like this for OA?

11 Answers, 1 is accepted

Sort by
0
Dimitar Kapitanov
Telerik team
answered on 10 Jan 2009, 08:25 AM
Hi MWM,
Can you elaborate a bit regarding your idea? I am not sure I understand correctly, though it seems interesting. Could you provide more details, even a snippet of code what exactly do you think we should have added for OpenAccess. We have code generation features, and also provide sample code base (helpers, ADS support) with our distribution, so do you think we should have this implementation also bundled inside?

Greetings,
Dimitar Kapitanov
the Telerik team

Check out Telerik Trainer, the state of the art learning tool for Telerik products.
0
MWM
Top achievements
Rank 1
answered on 10 Jan 2009, 05:43 PM
Dimitar,

Thank you for your response.  Let me try to convey where I'm coming up short in using an ORM (such as OA) and what I'd like to see available right out of the "box".

ASP.NET is a great technology offering huge flexibility, but I have ALWAYS (since 2003) found it to be a pain when it comes to working with data from a database.  I remember starting out with using table adapters and filling them, etc.  It has always seemed to be a real headache to handle the plumbing for data access that I've had to go through before I could ever get to what I really wanted and needed to do -- write a program which does something with that data!

Enter ORMs... especially OA since I'm already working with Telerik's controls.  I think the initial capability of OA's wizards is pretty good.  It will take my already-created database and generate code to map classes to each table within the database.  This is great!

But what I really need to be able to do right after that, in order to be productive, is have the wizard(s) go a few steps further in helping me to generate all the basic code to where I can immediately begin coding the program and accessing data.  I know OA is providing some of this, but for example, the simplest need and most common (to me at least) is that I want to grab 1 or more records from a given table (and possibly any related tables using lazy loading) - without having to go write OQL, SQL, or LINQ, at least for single-record retrieval using an ID.

My understanding in OA is that I must first start writing either OQL or SQL (or LINQ I guess) queries into the partial classes (or somewhere) before I can have the ability to do even the simplest thing... retrieve a single record based on its ID.

Before I ran into OA, I was giving serious consideration to NHibernate.  Not sure what you know about it and my knowledge is limited, but it does offer many similar things (except for VS integration and visual wizards - both of which are key for me) as OA.  One can use additional (also free) tools with NH to gain similar "automation" of code generation such as ActiveWriter.

After all is said and done, with NH, I can have similar code generated as OA for my database tables.  But, it's at that point that NH does something for me that I would like to see OA do.  NH provides defaulted methods to do simple records retrieval, updates and storage.  It has default methods which allow me to retrieve a single row from a table using an ID field value.  This type of functionality is very important as it requires me to write no plumbing code, it's just ready to go.

Take a look at a snippet from the NH docs:

Cat cat = (Cat) sess.Get(typeof(Cat), id);  // <--- this is a generic method (Get) as you must provide type, but it works for all

if (cat==null) {

cat = new Cat();

sess.Save(cat, id);

}

return cat;


In a single line of code, I can "Get" using a known ID.  Then I can modify/create as needed.  I know OA does simplify some of these types of things, but I'm unsure how complete it is.

LLBLGen Pro generates code (base classes of mappings to tables), but then also generates starting methods for retrieving/querying in many different ways by default, defaults which fulfill nearly all types of needs for getting and putting data.

I would urge you to look at NH.  From my limited knowlege, it best compares to that of OA.  Thing is... NH doesn't need the bizarre word "extent" to get things done; I would question why OA would then.

To get back to your question, I think your wizards are a good start, but I'd like to see them evolve with more options (e.g. allow me to input a "base" class name instead of just a namespace, or allow me to at least specifiy the target folder in my project for better organization (I keep having to move it to a folder)).

I know many are much more knowledgable and like to do more themselves, but for others who are as experienced or don't have the need/time to do that, I'd like to see the wizards at least have the options to generate more code, such as the "helpers" that you referred to as being in your sample code.

Why not generate a complete model of code (perhaps broken out into directories following the standard, well-accepted 3-layer architecture) which would require the programmer to do as little as possible and have data plumbing code ready to go instantly without having to write anything?  Isn't the reality that most if not all projects are going to want/need the "helper" type of code to optimally manage a single session factory?  I want to be able to generate from your wizards to the point that all I need to do is set a reference in my projects to the data access project and go!  I want to be able to immediately call a method to create/get a "scope", starting a transaction if I like, get a piece of data (e.g. 1 or multiple rows of data), modify it and persist it.

If OA can do all of this now, please put together a simple and I mean simple example.  A helpful example would involve 3-layer architecture and a solution with 2 projects, one for data access (all code generated by OA) and another ASP.NET (C#) project which has a form (aspx) which manages two tables from the db which are related such as a "customer" with "orders" (not even which necessarily includes order details (a third related detail table) just to keep it simple).  In the OA project portion, include the helper class or whatever's needed to get things done out of the box.  Above all, make sure the thing can compile and run and show me something without my having to wade through and try to correct a bunch of code (the example I downloaded required just that and I never got it to work, plus it was way too involved for me at least to start).

The generics portion I mentioned...

It would be nice to have a single class which uses generics and has basic functionality for ALL my db table to use.  I was suggesting the auto-generation of a generics class as part of your wizard which has a "getById" and "getAll" and so forth for handling what I would consider the bulk of basic needs for data access/management in general.  By using generics, it would not be necessary to have these same methods in each of the generated classes, but into a single base class.  Of course, if the wizard is generating the code, it could do it for all classes being mapped, that's up to you.  In any case, I'm striving for simplistic, out-of-the-box-functionality which allows for simple data access to:

1.  Retrieve a single record based on its ID
2.  Retrieve multiple records based on a particular criteria (e.g. "names starting with K")
3.  Create/alter records and persist them
4.  Delete records and optionally all related ones (i.e. cascading).

Again, it might be prudent to at least look at the docs for NH (http://www.hibernate.org/hib_docs/nhibernate/1.2/reference/en/pdf/nhibernate_reference.pdf) to see how the wizards / OA in general might fulfill some of the things I've mentioned (assuming it doesn't now and that I haven't missed it).

Let me know your thoughts and if I can clarify anything.  Keep in mind I am no expert and require all the assistance from wizards to be productive as possible.  I wish only to rid the work involved in getting/putting data from/to a database in ASP.NET.

Thanks!
0
Robert
Top achievements
Rank 2
answered on 13 Jan 2009, 08:50 PM
Hi There,

I know exactly what you're looking for and I wanted the same thing - out of the box, one-liner persistence.  As a result, I created the following PersistenceManager that uses Generics.  Let me know what you think and if you have any suggestions for improvement.

Most of the methods should be self-explanatory but others are trickier (it's a work in progress).  Here's an example of a trickier call:

persistenceManager.GetWithForeignKeyFilter<Domain.Office, Domain.Company>("*", "Office.Company = $1", applicationService.Company.CompanyID);    

This above gets me the office where the Company foreign key/relationship on the domain object has a value of CompanyID.

Here's another:
var args = new PersistenceManager.QueryParam[2];
 args[0] = new PersistenceManager.QueryParam(applicationService.HomeOffice.OfficeID, typeof(Domain.Office));
result =       persistenceManager.Get<Domain.Lookup>(
                        "SELECT * from LookupExtent WHERE Office = $1 ORDER BY DisplayText",
                        args);

This above gets all lookup domains with the where clause using parameterized arguments.

Best Regards,
Robert

------------8<   snip snip >8-------------------------------------------------
using System;
using System.Collections.ObjectModel;
using Telerik.OpenAccess;

namespace XXXXX
{
    public class PersistenceManager : IPersistenceManager
    {
        public class QueryParam
        {
            public Type SourceEntityType { get; set; }
            public object Value { get; set; }

            public QueryParam(object value)
            {
                this.Value = value;
            }

            public QueryParam(object value, Type sourceEntityType)
                : this(value)
            {
                SourceEntityType = sourceEntityType;
            }
        }
        public PersistenceManager()
        {

        }

        /// <summary>
        /// Retrieves all of the typed entities from the database using the Application context.
        /// </summary>
        /// <typeparam name="T">The type of entity to retrieve.</typeparam>
        /// <returns>An observable collection of type T.</returns>
        public ObservableCollection<T> GetAll<T>() where T : new()
        {
            return GetAll<T>(ScopeContext.Application);
        }

        /// <summary>
        /// Retrieves all of the typed entities from the database using the Application context.
        /// </summary>
        /// <typeparam name="T">The type of entity to retrieve.</typeparam>
        /// <param name="context">The scope context to use.</param>
        /// <returns>An observable collection of type T.</returns>
        public ObservableCollection<T> GetAll<T>(ScopeContext context) where T : new()
        {
            ObservableCollection<T> resultList;
            IQueryResult result = null;
            string query;

            var scope = ObjectScopeProvider.ObjectScope(context);

            try
            {
                if (!scope.Transaction.IsActive)
                {
                    scope.Transaction.Begin();
                }

                query = "SELECT * FROM " + typeof(T).Name + "Extent";
                result = scope.GetOqlQuery(query).Execute();

                resultList = new ObservableCollection<T>();

                foreach (var c in result)
                {
                    resultList.Add((T)c);
                }
            }
            finally
            {
                if (result != null)
                {
                    result.Dispose();
                }
            }

            return resultList;
        }

        /// <summary>
        /// Retrieves all of the typed entities from the database using the Application context and filter.
        /// </summary>
        /// <typeparam name="T">The type of entity to retrieve.</typeparam>
        /// <param name="context">The scope context to use.</param>
        /// <param name="whereClause">The filter for the retrieve.</param>
        /// <returns>An object type T.</returns>
        public T Get<T>(ScopeContext context, string whereClause) where T : new()
        {
            var resultObject = default(T);
            IQueryResult result = null;

            var scope = ObjectScopeProvider.ObjectScope(context);

            try
            {
                if (!scope.Transaction.IsActive)
                {
                    scope.Transaction.Begin();
                }

                string query = string.Format("SELECT * FROM {0}Extent WHERE {1}", typeof(T).Name, whereClause);
                result = scope.GetOqlQuery(query).Execute();

                var i = result.Count; // Force the fetch

                if (i > 1)
                {
                    throw new InvalidOperationException(
                        "The where clause resulted in more than 1 item returned.  This is not valid with this method.");
                }

                if (i > 0)
                {
                    resultObject = (T)result[0];
                }

            }
            finally
            {
                if (result != null)
                {
                    result.Dispose();
                }
            }

            return resultObject;
        }

        public ObservableCollection<T> Get<T>(string query)
        {
            return Get<T>(ScopeContext.Application, query, null);
        }

        public ObservableCollection<T> Get<T>(string query, params QueryParam[] args)
        {
            return Get<T>(ScopeContext.Application, query, args);
        }

        /// <summary>
        /// Retrieves all type entities matching the provided query and parameters.
        /// </summary>
        /// <typeparam name="T">The type of entity to retrieve.</typeparam>
        /// <param name="context">The scope context to use.</param>
        /// <param name="query">The query to filter the entities.</param>
        /// <param name="args">Arguments for the query.</param>
        /// <returns>An observable collection of type T.</returns>
        public ObservableCollection<T> Get<T>(ScopeContext context, string query, params QueryParam[] args)
        {
            ObservableCollection<T> resultList;
            IQueryResult result = null;
            object[] processedArgs;

            var scope = ObjectScopeProvider.ObjectScope(context);

            try
            {
                if (!scope.Transaction.IsActive)
                {
                    scope.Transaction.Begin();
                }

                if (args != null)
                {
                    // We need to convert any keys to OIDs for OpenAccess to be happy
                    processedArgs = new object[args.Length];

                    for (int i = 0; i < args.Length; i++)
                    {
                        if (args[i].SourceEntityType != null)
                        {
                            processedArgs[i] = Database.OID.ParseObjectId(args[i].SourceEntityType, args[i].Value.ToString());
                        }
                        else
                        {
                            processedArgs[i] = args[i].Value;
                        }
                    }

                    result = scope.GetOqlQuery(query).Execute(processedArgs);
                }
                else
                {
                    result = scope.GetOqlQuery(query).Execute();
                }

                resultList = new ObservableCollection<T>();

                foreach (var c in result)
                {
                    resultList.Add((T)c);
                }
            }
            finally
            {
                if (result != null)
                {
                    result.Dispose();
                }
            }

            return resultList;
        }


        /// <summary>
        /// Retrieves all of the typed entities filtered by the where clause and the single foreign key.
        /// </summary>
        /// <typeparam name="T">The type of entity to retrieve.</typeparam>
        /// <typeparam name="FT">The type of the referenced entity to filter by.</typeparam>
        /// <param name="columns">The columns to retrieve.  Use '*' for all columns.</param>
        /// <param name="whereClause">The filter clause.</param>
        /// <param name="foreignKeyValue">The foreign key value.</param>
        /// <returns>An observable collection of type T.</returns>
        public ObservableCollection<T> GetWithForeignKeyFilter<T, FT>(string columns, string whereClause, Guid foreignKeyValue) where T : new()
        {
            return GetWithForeignKeyFilter<T, FT>(ScopeContext.Application, columns, whereClause, string.Empty, foreignKeyValue);
        }

        /// <summary>
        /// Retrieves all of the typed entities filtered by the where clause and the single foreign key.
        /// </summary>
        /// <typeparam name="T">The type of entity to retrieve.</typeparam>
        /// <typeparam name="FT">The type of the referenced entity to filter by.</typeparam>
        /// <param name="columns">The columns to retrieve.  Use '*' for all columns.</param>
        /// <param name="whereClause">The filter clause.</param>
        /// <param name="orderByClause">The order by clause.</param>
        /// <param name="foreignKeyValue">The foreign key value.</param>
        /// <returns>An observable collection of type T.</returns>
        public ObservableCollection<T> GetWithForeignKeyFilter<T, FT>(string columns, string whereClause, string orderByClause, Guid foreignKeyValue) where T : new()
        {
            return GetWithForeignKeyFilter<T, FT>(ScopeContext.Application, columns, whereClause, orderByClause, foreignKeyValue);
        }

        /// <summary>
        /// Retrieves all of the typed entities filtered by the where clause and the single foreign key.
        /// </summary>
        /// <typeparam name="T">The type of entity to retrieve.</typeparam>
        /// <typeparam name="FT">The type of the referenced entity to filter by.</typeparam>
        /// <param name="context">The scope context to use.</param>
        /// <param name="columns">The columns to retrieve.  Use '*' for all columns.</param>
        /// <param name="whereClause">The filter clause.</param>
        /// <param name="orderByClause">The order by clause.</param>
        /// <param name="foreignKeyValue">The foreign key value.</param>
        /// <returns>An observable collection of type T.</returns>
        public ObservableCollection<T> GetWithForeignKeyFilter<T, FT>(ScopeContext context, string columns, string whereClause, string orderByClause, Guid foreignKeyValue) where T : new()
        {
            ObservableCollection<T> resultList;
            IQueryResult result = null;
            string query;

            var scope = ObjectScopeProvider.ObjectScope(context);

            try
            {
                if (!scope.Transaction.IsActive)
                {
                    scope.Transaction.Begin();
                }

                query = string.Format("SELECT {0} FROM {1}Extent as Office {2} {3}", columns, typeof(T).Name, string.IsNullOrEmpty(whereClause) ? "" : " WHERE " + whereClause, orderByClause);
                result = scope.GetOqlQuery(query).Execute(Database.OID.ParseObjectId(typeof(FT), foreignKeyValue.ToString()));

                resultList = new ObservableCollection<T>();

                foreach (var c in result)
                {
                    resultList.Add((T)c);
                }
            }
            finally
            {
                if (result != null)
                {
                    result.Dispose();
                }
            }

            return resultList;
        }

        /// <summary>
        /// Retrieves the typed entity by its primary key.
        /// </summary>
        /// <typeparam name="T">The type of entity to retrieve.</typeparam>
        /// <param name="primaryKey">The primary key value to filter by.</param>
        /// <returns>The resulting entity that matches the primary key, or null.</returns>
        public T GetByPrimaryKey<T>(Guid primaryKey) where T : new()
        {
            return GetByPrimaryKey<T>(primaryKey, ScopeContext.Application);
        }

        /// <summary>
        /// Retrieves the typed entity by its primary key.
        /// </summary>
        /// <typeparam name="T">The type of entity to retreive.</typeparam>
        /// <param name="primaryKey">The primary key value to filter by.</param>
        /// <param name="context">The scope context to use.</param>
        /// <returns>The resulting entity that matches the primary key, or null.</returns>
        public T GetByPrimaryKey<T>(Guid primaryKey, ScopeContext context) where T : new()
        {
            var scope = ObjectScopeProvider.ObjectScope(context);

            IObjectId oid = Database.OID.ParseObjectId(typeof(T), primaryKey.ToString());
            T record = (T)scope.GetObjectById(oid);

            return record;
        }

        /// <summary>
        /// Retrieves data using direct sql.
        /// </summary>
        /// <param name="context">The scope context to use.</param>
        /// <param name="sql">The sql to execute.</param>
        /// <returns>The query result.</returns>
        public IQueryResult GetBySQL(ScopeContext context, string sql)
        {
            var scope = ObjectScopeProvider.ObjectScope(context);
            IQuery sqlQuery = scope.GetSqlQuery(sql, null, null);

            IQueryResult result = sqlQuery.Execute();

            return result;
        }

        /// <summary>
        /// Retrieves data using direct sql. Uses defult (Application) context
        /// </summary>
        /// <param name="sql">The sql to execute.</param>
        /// <returns>The query result.</returns>
        public IQueryResult GetBySQL(string sql)
        {
            return GetBySQL(ScopeContext.Application, sql);
        }

        /// <summary>
        /// Remove (Delete from DB) this object on next commit.
        /// </summary>
        /// <param name="objectToRemove">the object to remove</param>
        public void Remove(object objectToRemove)
        {
            var scope = ObjectScopeProvider.ObjectScope();

            scope.Remove(objectToRemove);
        }

        /// <summary>
        /// True if the there are any un-commited changes, false otherwise.
        /// </summary>
        public bool TransactionIsDirty
        {
            get
            {
                var scope = ObjectScopeProvider.ObjectScope();
                return scope.Transaction.IsDirty;
            }
        }

        public void TransactionRollback()
        {
            var scope = ObjectScopeProvider.ObjectScope();
            scope.Transaction.Rollback();
        }

        /// <summary>
        /// Saves the entity to the database.
        /// </summary>
        /// <typeparam name="T">The type of entity to retrieve.</typeparam>
        /// <param name="entityToSave">The entity to save.</param>
        public void Save<T>(T entityToSave)
        {
            var scope = ObjectScopeProvider.ObjectScope();

            Save(entityToSave, scope);
        }

        /// <summary>
        /// Saves the entity to the database.
        /// </summary>
        /// <typeparam name="T">The type of entity to retrieve.</typeparam>
        /// <param name="entityToSave">The entity to save.</param>
        /// <param name="context">The scope context to use.</param>
        public void Save<T>(T entityToSave, ScopeContext context)
        {
            var scope = ObjectScopeProvider.ObjectScope(context);

            Save<T>(entityToSave, scope);
        }

        private void Save<T>(T entityToSave, IObjectContext objectsScope)
        {
            try
            {
                objectsScope.Add(entityToSave);                
            }
            catch
            {
                objectsScope.Transaction.Rollback();
                throw;
            }
            finally
            {
                objectsScope.Transaction.Commit();
            }
        }

        public void CleanUp()
        {
            ObjectScopeProvider.DisposeScopes();
        }
    }
}

0
Dimitar Kapitanov
Telerik team
answered on 16 Jan 2009, 07:46 AM
Hi Mitchell,
First of all I wanted you guys to know that we tried to prepare a small KB article that addresses the your request. You can find it here. It includes the API examples you requested: how to retrieve objects from the O/R mapper.
Please give us feedback whether this is what you want. Regarding the additional steps in the wizard we are working exactly on providing a more fluent work flow and experience there, and have additional 'helper' step at the end. Also we are planning to enhance our API exactly in the direction you mentioned, also providing "fluent" behavior, but presently I'm unable to provide you with an exact time-frame for this.

Greetings,
Dimitar Kapitanov
the Telerik team

Check out Telerik Trainer, the state of the art learning tool for Telerik products.
0
MWM
Top achievements
Rank 1
answered on 18 Jan 2009, 09:33 PM
Robert,

This looks right along the lines of what I was speaking of.  Based on Telerik's response to this discussion, it sounds like they may be consdering the addition of such things.

Thank you for sharing the code!  Good to know I wasn't alone in this line of thinking.
0
MWM
Top achievements
Rank 1
answered on 18 Jan 2009, 09:36 PM
Dimitar,

Great to hear!  I'm looking forward to seeing these improvements in a (near-) future release.

While you're "passing along" suggestions, be sure to talk to the developers and have them look at their lexical analyzer and alter the line which requires the bizarre word "Extent" to be added to table names, you know, just to avoid freaking people out who are new to OA... :-)
0
Dimitar Kapitanov
Telerik team
answered on 20 Jan 2009, 06:32 AM
Hello MWM,
By "extents" in the O/R domain are named the entities that are produced from a collection/table from the data store. For example the CustomerExtent, really means that we will address entities of type Customer, say from a Customers table. I am not sure whether the comparison is really correct but you can think of CustomerExtent as of CustomerExtent equals Customers table . That said I know it is a bit awkward to have that in the API, but it is a natural part of the OQL language. Please  elaborate a bit on how you expect things to work for you. I really think that it is not so easy to address the lexical parser, but we have it in the plans for our API enhancements.

All the best,
Dimitar Kapitanov
the Telerik team

Check out Telerik Trainer, the state of the art learning tool for Telerik products.
0
MWM
Top achievements
Rank 1
answered on 23 Jan 2009, 04:06 PM
Dimitar,

Not to drag this issue out, but I must admit, having to add the word "extent" to a table name in a query has been entirely bizarre to me from my first encounter with OpenAccess.  Adding that changes the true name/appearance of the table name.  What's more is that my first read in the OA help/manual showed a brief example with it added and with no explanation.  I thought the name of the table was something like "EmpExtent".  Later I found it's just an appendage.

Given all of this, I thought I'd check for myself to see whether this is truly a part of the official OQL standard language definition.  Here's the response I received from the ODBMS.ORG:

"Hello Roberto

I was just looking over the definition of the OQL   
standard, which is in ODMG 3.0 pages 89-132 and I see no mention of having to   
include the string "Extent" on an identifier used in a from clause.
I double-checked the syntax definition, which is in section 4.12.2 (pp. 126-132)   
and I found no mention of "Extent" being required as part of an identifier. This   
sounds like a vendor-specific requirement.

-Mike Card"


Seems that Vonatec (or Telerik?) has added this appendage.  Thing is that with a "where" clause in the query statement, one is no longer grabbing the "extent" or "entirety" of the table, yet the word still stands; it must be added.

Just thought I'd share this with you.  From what I see, OA uses some non-standard OQL.  I don't think it would take much to alter in the lexical analyzer / parser to rid this oddity or even make it optional.  NHibernate operates in a similar fashion as OA and it doesn't require it.

Thanks for the "ear"!
0
Dimitar Kapitanov
Telerik team
answered on 25 Jan 2009, 08:56 AM
Hello MWM,
Thanks a lot for your comment. I see your point very clearly. I will talk to my colleagues, and see why the "Extent" extension was needed, and then will provide you with their feedback. I also think that this will not be very complicated to change, with some additional stuff regarding the OQL implementation. However you should know that thorough LINQ support is with much greater priority on our plans, as it will be the main way of accessing data in the .NET domain. That means that all possible changes that are to be made for OQL, will be included possibly for Q3 2009 release. Hope that clears things a bit. Also when I have my colleagues comments regarding your proposal, I will notify you asap.

Sincerely yours,
Dimitar Kapitanov
the Telerik team

Check out Telerik Trainer, the state of the art learning tool for Telerik products.
0
MWM
Top achievements
Rank 1
answered on 26 Jan 2009, 07:25 AM
Dimitar - fair enough!  Thanks!
0
Dimitar Kapitanov
Telerik team
answered on 26 Jan 2009, 01:44 PM
Hi MWM,
It was my pleasure having all these discussion with you. I will keep you updated as we progress with your suggestions.

Regards,
Dimitar Kapitanov
the Telerik team

Check out Telerik Trainer, the state of the art learning tool for Telerik products.
Tags
General Discussions
Asked by
MWM
Top achievements
Rank 1
Answers by
Dimitar Kapitanov
Telerik team
MWM
Top achievements
Rank 1
Robert
Top achievements
Rank 2
Share this question
or