UtterAccess.com
X   Site Message
(Message will auto close in 2 seconds)

Welcome to UtterAccess! Please ( Login   or   Register )

Custom Search
2 Pages V < 1 2  (Go to first unread post)
   Reply to this topicStart new topic
> Global Variables, Access 2016    
 
   
GroverParkGeorge
post Dec 28 2017, 09:17 AM
Post#21


UA Admin
Posts: 33,786
Joined: 20-June 02
From: Newcastle, WA


It also turns out I was working under a misinterpretation (okay, I was wrong....)

"Global" is, apparently, and older keyword that has fallen out of favor and should be replaced by the keyword "Public". In other words, Public variables declared outside a procedure are, in scope, global to the application.
Go to the top of the page
 
JonSmith
post Dec 28 2017, 09:19 AM
Post#22



Posts: 3,915
Joined: 19-October 10



Broadly agree with Jack here. There are some cases where Tempvars are great. For example the ribbon ID which doesn't ever change and is more of a constant is good for a tempvar since it is immune to state loss and if you reset alot during Dev that can be handy. There are also some situations where they can be useful in queries, I have used them in a limited scope for that before when wanting to dynamically assign a filter but didn't want to use a form control or something like that.

If you are looking to switch all your globals to Tempvars though, as Jack says, I think you are maybe trying to treat the symptoms of bigger issues.
Go to the top of the page
 
BruceM
post Dec 28 2017, 10:09 AM
Post#23


UtterAccess VIP
Posts: 7,683
Joined: 24-May 10
From: Downeast Maine


Jack and Jon, I just wanted to point out that TempVars are an option with the two examples the OP provided. I never intended to present them as a cure-all or a solution for an unstable application. Of course it is best if the application does not falter in such a way that public (global) variables are reset, because such faltering indicates problems beyond the issue of global variables.

I didn't use global variables very much before TempVars came along. I don't use TempVars very much now. They are just one of the options.
Go to the top of the page
 
pdanes
post Dec 28 2017, 11:31 AM
Post#24



Posts: 81
Joined: 19-June 10



jleach

Global variable use is not a problem of any sort. They're perfectly valid constructs and an excellent solution to many design problems.

Ribbon loss of scope is pain in the patoot. I don't see why the ribbon can't be reloaded just like all other things are after a reset. All that's necessary is to re-read the USysRibbons table and fire the OnLoad ribbon event. MS dropped the ball on that one.
This post has been edited by pdanes: Dec 28 2017, 11:43 AM
Go to the top of the page
 
pdanes
post Dec 28 2017, 11:42 AM
Post#25



Posts: 81
Joined: 19-June 10



Global

The way I've always used the word, and seen the word used, is in reference to scope. A variable with a global scope is visible to all of the application as long as it exists, not necessarily that it -will- exist for the life of the application. A local variable is visible only in some more limited context. Maybe some better terminology is needed to distinguish between variables that are global in reach but limited in lifetime, as opposed to those global in reach and always viable. Much of the terminology used today is still a holdover from decades ago, when most of the constructs we routinely use today didn't even exist.

I would also welcome the addition of a write-once variable, as many other languages have. I know, I can make one using a class module, and have done so a few times, but it's slower to execute and not as clean to code.
This post has been edited by pdanes: Dec 28 2017, 11:44 AM
Go to the top of the page
 
theDBguy
post Dec 28 2017, 11:44 AM
Post#26


Access Wiki and Forums Moderator
Posts: 73,501
Joined: 19-June 07
From: SunnySandyEggo


Hi,

Pardon me for jumping in... I haven't used this method myself, but I wonder if it would help with reloading the Ribbon after a reset.

LoadCustomUI

Just curious...
Go to the top of the page
 
pdanes
post Dec 28 2017, 11:49 AM
Post#27



Posts: 81
Joined: 19-June 10



That looks interesting. Looks like it makes the ribbon completely dynamic. I wonder how fast it is if you have complex ribbons and switch between forms with different ribbons. Do you know if such a ribbon stays put through switching or closing/reopening a form that uses it?
Go to the top of the page
 
theDBguy
post Dec 28 2017, 11:50 AM
Post#28


Access Wiki and Forums Moderator
Posts: 73,501
Joined: 19-June 07
From: SunnySandyEggo


Hi,

Unfortunately, I haven't tried using it myself, so I really have no comment on how well it works.

Good luck!
Go to the top of the page
 
jleach
post Dec 28 2017, 12:12 PM
Post#29


UtterAccess Editor
Posts: 9,934
Joined: 7-December 09
From: Staten Island, NY, USA


>> They're perfectly valid constructs and an excellent solution to many design problems. <<

They are perfectly valid constructs, yes. As to excellent solutions to many design problems? Only if the person doesn't know any better. An excellently constructed codebase no place for global variables.

Typically there's a small handful (1-5) of global public properties (not variables, but properties, generally of the self healing kind: the actual variables backing the properties should be private) that expose application instance information (current version, that type of stuff). This doesn't qualify as many, I wouldn't guess, and everything else should be properly scoped (e.g., not global). This is not just Access/VBA, btw, but true for every programming language that has any case of scope (pretty much everything since the BASIC days of GOTO).

Cheers,
Go to the top of the page
 
jleach
post Dec 28 2017, 12:13 PM
Post#30


UtterAccess Editor
Posts: 9,934
Joined: 7-December 09
From: Staten Island, NY, USA


Not sure about ribbon use in forms, but the LoadCustomUI function is the goto for redrawing shell level ribbon from scratch (used to make "heavy" dynamic ribbon changes after initial app load: loading a brand new ribbon, for example)
Go to the top of the page
 
pdanes
post Dec 28 2017, 05:09 PM
Post#31



Posts: 81
Joined: 19-June 10



"An excellently constructed codebase no place for global variables."

That's exactly the kind of knee-jerk response that indicates very little thought went into the matter. It's one of those 'truisms' that people repeat to each other to make themselves feel better about something they do, or something they heard. But it's not true, and blind adherence to such sayings leads to convoluted constructs that are hard to write and much harder to maintain.

Globals have their place, and in many cases are the best way to do something; 'best' by any measure that does not include ritual obedience to a phrase, i.e. faster, clearer in function, easier to maintain, easier to test and modify their use and purpose. I use them, always with the prefix gbl_ for clarity, and I have yet to have a problem from the simple fact of them being globals.

Sometimes evolution of the design indicates that something other than a global variable is a better choice, and I make the change. Sometimes, something that started life as a class or whatever winds up being changed to a global, because as the design progressed, it became clear that was better.

Global variables are a tool, like any coding construct. They can be (and are) misused, like any coding construct. But blindly writing them off as 'bad' is just not very smart.
Go to the top of the page
 
kfield7
post Dec 28 2017, 09:04 PM
Post#32



Posts: 891
Joined: 12-November 03
From: Iowa Lot


I apologize in advance for jumping in, as this will not really answer anyone's questions (settle arguments?) here, but instead offer an alternative approach.
Since I have been using Access well before tempvars was implemented, I have never used tempvars, because I have not needed them.
Instead, if I want persistent data, and often do, I do what Access was designed to do: I store it in a table. This works in every version of Access, and I'm guessing that's what Access probably does with tempvars internally.
I set up a table of "variables", and use a UDF to store and retrieve specific values.

tblSysLocalVar
VarName (text, name of variable, I tend to keep this short)
VarValue (text, holds variable value, as text even if numeric, I translate on demand, and if this was actually a problem I'd just have a separate table for numeric values, but haven't to date).
VarDesc (text, lengthier description of purpose of the variable)

This may not be as efficient as tempvars, I don't know, but in 99.99% of cases since it's generally not used in a loop and there's not a lot of variables to sift through, I have never had any perceptible speed issues.
If it's needed in a loop, it's retrieved into a more appropriate actual variable then used.

BTW, with respect to the discussion on "local" versus "global", my choice of table name "tblSysLocalVar" is because it is a table local to the user (front end) rather than the back end.
Go to the top of the page
 
pdanes
post Dec 29 2017, 11:38 AM
Post#33



Posts: 81
Joined: 19-June 10



A table of global values is also a useful method, and one that I've implemented several times. So are document variables, and document properties, both custom and built-in. Lots of problems have several possible solutions, with no one of them clearly the 'best' one.

In most cases, I'd say the best method is the one that the author feels most comfortable using, because they're most likely to write good code that way, and least likely to make complicated and obscure errors, simply because they know what they're doing with it.

I'm currently rather intrigued by TempVars, largely because they're new to me, and the added benefit of being able to use them directly in SQL queries is very attractive. I have several times had to write functions specifically to retrieve such global information, because I couldn't address any of my other favorite constructs via SQL. Or I write a parameter query, but feeding the parameter into the query is several extra steps. Being able to write a query that will directly use a value created somewhere else in VBA would make some of these much simpler.

CODE
SpecimenAutoID = someValue
Typ = "something"
then
CODE
Set qdf = CurrentDb.QueryDefs("VyberLidiSpecimen")
With qdf
    .Parameters("SoucasnySpecimenAutoID") = SpecimenAutoID
    .Parameters("SoucasnyTyp") = Typ
    Set rst = .OpenRecordset
End With


could be replaced by:

CODE
TempVars("SpecimenAutoID ") = someValue
TempVars("Typ") = "something"
then
CODE
Set rst = CurrentDb.QueryDefs("VyberLidiSpecimen")


with the reference to the TempVar 'Typ' written right in the SQL of the query. One line of code instead of six, and no unnecessary creation of the query object - much cleaner, less prone to errors, and probably faster.
This post has been edited by pdanes: Dec 29 2017, 11:43 AM
Go to the top of the page
 
JonSmith
post Dec 29 2017, 11:55 AM
Post#34



Posts: 3,915
Joined: 19-October 10



Bruce, I think I agree with your sentiments so sorry if it came across as I was disagreeing. I mentioned that tempvars can be great but you should be used appropriately. Same goes true of globals.
I don't use many globals I keep my scopes typically smaller and more local.

QUOTE
I would also welcome the addition of a write-once variable, as many other languages have. I know, I can make one using a class module, and have done so a few times, but it's slower to execute and not as clean to code.


I'm confused by this, thats exactly what a procedure level variable is isn't it. You just declare it in that proc and it only gets used there and is destroyed when the proc ends....?


I will disagree that the best method is whatever the coder is most comfortable with. VBA has large numbers of people with bad practise. I recently reviewed the code of an applicant who's code example didn't compile (even though Option Explicit was present), the compile issues were due to variables not being declared and in one procedure the declaration spelling didn't match the actual use, which was literally the next line.
I am sure this person was more comfortable not declaring the variables or compiling cause he found it the easiest method and he could follow the code. Should it be tolerated by anyone trying to be professional, no.
Globals shouldn't just be thrown around everywhere cause someone is poor at writing code, I have seen this many times and is why I hesitate at large useage of them. This is not to say your code is bad or unprofessional, it could be great and totally appropriate. I just see it as a red flag and I see an even bigger red flag if they globals are frequently lost and someone wants to switch to tempvars to solve it.
Go to the top of the page
 
pdanes
post Dec 29 2017, 01:16 PM
Post#35



Posts: 81
Joined: 19-June 10



That's fine - there is no requirement anywhere that we agree on this, or on anything, for that matter.

No, globals should not be tossed into a mix because 'someone is poor at writing code'. If someone is not good at coding, they will still not be good at it, even if they use globals. And if some IS good at it, they will be good at it with globals as well. Coding skills and the use of global variables are not related.

I don't think your example of a person not compiling his code is at all the same as whether or not global variables are used. There are things that are just plain wrong, no matter who does them, or when. Not using Option Explicit is one such, in my opinion - I can think of no justification to ever omit that. Not compiling your code is another. If your code doesn't even compile, it's faulty, and has no business being released. When I wrote '...the best method is the one that the author feels most comfortable using...', I meant coding styles, selection among legitimate construct options, naming conventions and that sort of thing. I most certainly DID NOT mean deliberately creating garbage.

As for the write-once business, maybe I didn't express myself correctly. I meant something halfway between a constant and a variable (local or global), that can be written to ONCE, and then never again changed. SCALA has such a construct: a -var- can be manipulated endlessly, a -val- can be written to once, and that value is then fixed for the rest of its life. I believe JAVA has something like that as well, although I know almost nothing about JAVA. (Actually, I don't know much about SCALA, either. I'm trying to learn it, because it looks neat, but I'm suffering from a lack of anything purposeful to use it for.)

In VBA, it's possible to write a class that will detect and reject any attempts to write to an element of the class beyond the first one, and I have done that a few times to make my own home-grown WriteOnce variables, but I don't like it. It's awkward and slow, since it requires a subroutine call and several lines of VBA code therein to make a simple value assignment, the same every time I subsequently read the value, and the same again to reject any additional write attempts. I think that VBA would benefit from an optional write-once property of variables, and if I was employed in any capacity at Microsoft where I had influence on the matter, I would be pushing for such an addition.

And once again, the TempVars is not intended to get around code that is faulty or crashing unexpectedly. The major thing that caught my attention is that they can be used directly in queries. It would also be nice if I could use them for ribbon references, so that I don't have to close and reopen a database every time I screw up something during development and have to reset. When I reset, I would like that to mean reset EVERYTHING, and start again with a clean slate, just like when the DB was first opened. That simply does not happen with the ribbon. When I do a reset, all references to the ribbon are lost, permanently. In fact, I quote the very site to which you referred me a few days ago, RibbonStateLoss:
QUOTE
There is simply no easy, built-in way to recover the handle to the ribbon when there are problems in or with your code. The only way to fix it is to close and reopen your workbook, not a very user-friendly way.
If setting a TempVar reference to the ribbon object can prevent that reference from being lost, great - I will use it with enthusiasm. If not, they still look like useful constructs for other things, specifically making VBA-set values directly available to SQL.
Go to the top of the page
 
jleach
post Dec 29 2017, 02:21 PM
Post#36


UtterAccess Editor
Posts: 9,934
Joined: 7-December 09
From: Staten Island, NY, USA


Property getters and setters can be written for standard modules too, not just class modules:


CODE
'standard module named App
'container for startup, shutdown and "global" variables (as properties...)
Option Compare Database
Option Explicit

Private mCurrentUserID As Long

Public Property Get CurrentUserID() As Long
  CurrentUserID = mCurrentUserID
End Property

Public Property Set CurrentUserID(l As Long)
  'or private for readonly
  mCurrentUserID = l
End Property


Or you can do readonly self-healing:

CODE
Private mBackendPath As String

Public Property Get BackendPath() As String
  If mBackendPath = "" Then mBackendPath Ini.Read("Settings", "BackendPath")
  BackendPath = mBackendPath
End Property


These are not difficult to understand, are far safer to use than global variables, and are subtly important in that they're creating a level of abstraction between the interface and the implementation. Some of the worst Access projects that we take on are those that a) have tons of global variables, and/or b) have tons of ExpressionService or VBA function calls in queries (ES is the worst, especially with nested queries).

The first thing we do with global variables is wrap them in properties, isolate the variable itself, name the property the same as the original variable name. This gives us flow control over managing the variables as well as allowing us to see when it's being called (in practice) and when it's being set (in practice, as well as logging what it's being set to...).

Even with only a small set of "global" variables (app level state): why not wrap them in a property? It's not difficult, takes all of an extra 10 seconds, and simply by creating a setter and getter for them, you've allowed yourself to control them tightly from here on out, without it being any more difficult to use (in fact, you can now call them from queries if you want, without having to move them to tempvars, which makes the code much more portable - any idea how much code we've ported from VBA to C# or JS so we can run it on a webserver over the past 5 years? Lots - and it's a whole lot easier when the code follows normal industry-level programming practices).

Why wouldn't we do it? This is not a case of where I'd want to dumb down the code so someone inexperienced can understand it... there are times that's appropriate, but not here - rather this is a case where if someone's not familiar with the pattern, it's a good learning opportunity. I can think of no valid reason to ever have a global variable when such a simple g/s construct can be used at such an advantage (other than: the person who programmed it didn't know any better).


Global state almost always means that scope isn't being managed correctly. Sure, in many cases it's easy to toss in a global variable and use that to pass an object from form to opened child form (can't pass them via openargs, after all, right?). Again though, the reason people tend to do things like that is because they don't know they can expose a public property on the calling form and read it at ease from the called form. Then we have no scope pollution, and we've also taken a better step toward being able to break coupling between the two forms by using this. We can also do multi-instance forms without ever having to worry about it (you surely can't do multi-instance global variables...). If I used a global variable, I'd have coupled forms, I wouldn't be able to do multi-instance forms without lots of rewrite if I ever needed the feature, I'd not remember exactly what that global variable was even for three years later when I next run into it... Scope is very important to long-term maintainability of code (long-term being 5+ years: applications tend to have a pattern of maintainability issues depending on age, and usually between 5-10 years of general evolution is where the scope issues really start to hit very hard). Global variables almost always means that someone didn't know how to properly control it, or were lazy. The first is acceptable: we've all been there. The second is not. (after all, writing the code is the easy part: by then we already know what we need to do, and it doesn't time much extra time to "do it right")

There's so. many. reasons. not to use global variables. That's just a few small examples off the top of my head, with long term implications that we know of based on years of experience, dozens of languages, hundreds of projects: not knee jerk reactions or blind and thoughtless truisms.


I actually take the same unpopular-with-many-but-oh-well view towards having VBA specific stuff (especially tempvars and direct form references) inside queries. Rather, queries should be clean JET/ACE, because when the day comes where someone decides they want real disaster recovery (or pick any number of other reasons) and the database needs to be ported to SQL Server or PostrgreSQL (or whatever), it's going to mean the difference between a fairly painless port and a "sorry we need to rewrite the whole app" conversation with the client. Imagine costing a company hundreds of thousands of dollars ten years down the road because we're too lazy to do things correctly now? Not I. I've seen companies go under because of things like this. Not on my watch. I'll stick to no global variables and fairly pure JET/ACE, thanks.

Cheers
Go to the top of the page
 
pdanes
post Dec 29 2017, 08:00 PM
Post#37



Posts: 81
Joined: 19-June 10



QUOTE
Property getters and setters can be written for standard modules too, not just class modules:

Huh, didn't know you could do that. I always thought properties went with class modules, and never even thought to try it in a regular one. You've got an error in your CurrentUserID example, though: you've matched a Get and Set, which won't even compile. It should be Get and Let. Nor sure what use I can make of it at the moment, but something may come along, and it's always good to learn a new trick. Thanks.

On global variables, we're obviously just not going to agree. There is some information I want available to the entire application, and I see no simpler or clearer way to achieve that than by using a global variable. The user's name, privilege level and a few things like that come readily to mind, since I use those in many situations, all through the application. Sure, wrapping them in properties allows you to 'control' their use to some extent. So what? You can apply that same logic to EVERY variable in the entire application, global or local. Are you going to do away with variables entirely and make everything a class or property? I just don't see the benefit. Granted, I work for myself, by myself, write my own stuff, and very rarely have to work on someone else's code. If you do, and this method lets you get a handle on things more effectively, great, but if you find garbage code in an application full of global variables, I suspect the same author would have produced garbage even if he had not used a single global variable.

QUOTE
in fact, you can now call them from queries if you want

That's a plus, granted, although a reset blows them away again. For something that I might want to persist through a reset, TempVars seem a better choice.

QUOTE
because they don't know they can expose a public property on the calling form

I do know that, but don't see it as all that much of an improvement. It requires the called form to know something about the calling form, which violates the principle of encapsulation. Globals do too, of course, but if it's something like the user's name, that is set once on startup and used read-only in the exact same manner everywhere in the application, I don't see that as such a huge sin. Every application has to know something about its environment, and who is running it is often one such thing.
Also, it would be nice if OpenArgs worked in both directions, and was not limited to just one text string, but I doubt that's likely to change.

The issue of multi-instance forms is a legitimate concern in some cases, not in others. Again, using my favorite example of the user's name, I do not care how deeply nested a set of forms may be. If any instance needs the user's name, it will get it in exactly the same way: [something] = gbl_UserName, regardless of where it was called from, how many other instances are open, or anything, really. In fact, a global variable might well be used as a counter to keep track of how many instances of a form are open, should that information be required elsewhere in the code. Every instance adds 1 on open, and subtracts 1 on close. Sure, you could do the same with a class, public property, field in a table, document property, document variable, TempVar and probably several other things I haven't thought of just now. In fact, if you wanted more information on what was open, rather than a simple count, a class or some such would certainly be more appropriate.

And on the issue of porting and standards: sure, there are some legit arguments there, for some situations. However, the stuff I design is so unlikely to ever be ported that it makes no sense for me to spend any time stressing over it. I do not write for companies, but mostly for museum scientists and curators, who have their own specific datasets and needs, or occasionally for a single entrepreneur, who has some strange set of needs that off-the-shelf software either can't do, or comes as part of a huge and expensive package that is far outside his budget. Data is occasionally exported from one of my applications, and there of course I make every effort to maintain a clean design: tables fully normalized, proper relationships between them, strict and restrictive data types, meaningless primary keys, decent naming standards.

And I find no merit in the notion of restricting design options simply because a feature is not part of some standard. In fact, quite the opposite is true. You select a language or environment precisely BECAUSE it can do specific things that some other one cannot. Will FORTRAN programmers refrain from using complex variables in their programs because BASIC does not have such constructs? Will JAVA programmers stop subclassing because COBOL programmers cannot? Will RPG programmers stop using their program cycle indicators? If you're designing a business application with a fair expectation of seeing the business grow into needing something much more robust in the future, then it certainly makes sense to keep that in mind, but that is not the customer environment that I service. Several of my applications use an Access form to draw maps. It took some doing, but I got Access to manage the graphics to do that. Should I have refused to do so on the grounds that it was not standard behavior for databases? The customers for those applications are in heaven over the graphics, and have even published a number of scientific works using the maps that Access draws for them. I use whatever tools are available, and try to select the one that I think does the best job for a given situation. It may not be to everyone's taste, but that is one of the benefits of working for myself.
This post has been edited by pdanes: Dec 29 2017, 08:09 PM
Go to the top of the page
 
JonSmith
post Dec 30 2017, 09:01 AM
Post#38



Posts: 3,915
Joined: 19-October 10



Interesting discussion here, I agree with Jack up to his admittedly unpopular opinion that you shouldn't use non 'standard' stuff. I frequently have to code things in Excel for the company I work for because thats just how they want to work. Some people thing I should limit how I work with some stuff and not use things like Excel formula's etc, I don't agree, I would say an upscale to something like .net would require a big rebuild anyway. Make use of the best tools in the program you are working with.

pdanes, your comment
CODE
I just don't see the benefit. Granted, I work for myself, by myself, write my own stuff, and very rarely have to work on someone else's code. If you do, and this method lets you get a handle on things more effectively, great, but if you find garbage code in an application full of global variables, I suspect the same author would have produced garbage even if he had not used a single global variable.


I do have to work with other peoples code, alot of it is really bad and forced me to scrap it and start again. Like I said, globals are a red flag, not necessarily bad but I learnt to be wary of them. I don't think there are many examples of good use, your favourite user ID one is not one of them. I'll come back to that later.
I see what you are saying about compiling not being the same as globals. It wasn't my intention to conflate the two, I was addressing the point of whatever the author is most comfortable with is best. I think we agree that good quality is the best. Others people able to read and follow your code is essential and me and Jack both say, from experience of reading others which you admit is limited when it comes to your own experience, has frequently been an issue.


Now, as for your favourite global, the userID.
This could easily be a class with some properties in it about the user and their permissions (I assume thats why you track the user). In a module declare the class with the New keyword. Make the class get the userID itself on initialize so it needs no external procedures to feed it info, you obviously use Get properties to access the variables. Now you are sorted, try reseting your code, because the class is declared as New it will be fine after a reset from my experience.

Give us another global example?
Go to the top of the page
 
pdanes
post Dec 30 2017, 01:21 PM
Post#39



Posts: 81
Joined: 19-June 10



QUOTE
Make use of the best tools in the program you are working with.

Amen. Tools are there to be used - all of them. The fact that some people misuse them, or other packages don't have them is no reason not to use them.

QUOTE
…from experience of reading others which you admit is limited…

It is limited, but not completely negligible. I have occasionally had to deal with horrors written by others, and spent a good bit of time wishing I could get my hands around the author's throat. However, global variables have not once been a factor.

QUOTE
Give us another global example?

I could, but I see no point – you will have the same objections to anything I cite, and I have no desire to turn this discussion acrimonious. In any case, I'm not claiming that a class is a bad way to do this, just that it's often unnecessary. The example you cite is one such. I do it by declaring a global string variable, you would do it by declaring a global class variable. Sure, a class allows you to do more, but if you do not need to do more, it's pointless. I set the value of a string once on startup and read it in many places throughout the code. I would do exactly the same if I defined it as a class. All the class does is increase bulk, complexity and execution time. Impact is 100% negative, no benefit.
Go to the top of the page
 
2 Pages V < 1 2


Custom Search
RSSSearch   Top   Lo-Fi    21st October 2018 - 10:41 AM